[#1618] Settings redesign (#1658)

* [#1618] About redesign

* [#1618] Whats new redesign

* [#1618] Delete screen redesign

* [#1618] Export private data refactor

* [#1618] Seed recovery refactor

* [#1618] Seed Redesign

* [#1618] Feedback Redesign

* [#1618] Popup implementation

* [#1618] Localization fixes

* [#1618] Code cleanup

* [#1618] Code cleanup

* [#1618] Code cleanup

* [#1618] Documentation update

* [#1618] Code cleanup

* [#1618] Design hotfixes

* [#1618] Code cleanup

* [#1618] Test hotfixes

* [#1618] Test hotfixes

* Code cleanup

* Changelogs entries update

* Address few review comments

* Fix UI tests

* Fix bottom widget version name in WhatsNew

* Update Spanish texts

* Fix ktlint warnings

* Test hotfix

* Test hotfix

* Code cleanup

* Design hotfixes for small screens

---------

Co-authored-by: Honza <rychnovsky.honza@gmail.com>
This commit is contained in:
Milan 2024-11-14 14:12:59 +01:00 committed by GitHub
parent af5ed30e8a
commit 425052f1db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
103 changed files with 2508 additions and 2463 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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<String>,
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)
)
}
}
}
}
}
}

View File

@ -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 = {},
)
}

View File

@ -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() }
)
)
}
}

View File

@ -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

View File

@ -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
)

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M4,0.5L12,0.5A3.5,3.5 0,0 1,15.5 4L15.5,12A3.5,3.5 0,0 1,12 15.5L4,15.5A3.5,3.5 0,0 1,0.5 12L0.5,4A3.5,3.5 0,0 1,4 0.5z"
android:fillColor="#343031"/>
<path
android:strokeWidth="1"
android:pathData="M4,0.5L12,0.5A3.5,3.5 0,0 1,15.5 4L15.5,12A3.5,3.5 0,0 1,12 15.5L4,15.5A3.5,3.5 0,0 1,0.5 12L0.5,4A3.5,3.5 0,0 1,4 0.5z"
android:fillColor="#00000000"
android:strokeColor="#939091"/>
</vector>

View File

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M4,0.5L12,0.5A3.5,3.5 0,0 1,15.5 4L15.5,12A3.5,3.5 0,0 1,12 15.5L4,15.5A3.5,3.5 0,0 1,0.5 12L0.5,4A3.5,3.5 0,0 1,4 0.5z"
android:fillColor="#E8E8E8"/>
<path
android:strokeWidth="1"
android:pathData="M4,0.5L12,0.5A3.5,3.5 0,0 1,15.5 4L15.5,12A3.5,3.5 0,0 1,12 15.5L4,15.5A3.5,3.5 0,0 1,0.5 12L0.5,4A3.5,3.5 0,0 1,4 0.5z"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"/>
<path
android:pathData="M12,5L6.5,10.5L4,8"
android:strokeLineJoin="round"
android:strokeWidth="1.6666"
android:fillColor="#00000000"
android:strokeColor="#343031"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M4,0.5L12,0.5A3.5,3.5 0,0 1,15.5 4L15.5,12A3.5,3.5 0,0 1,12 15.5L4,15.5A3.5,3.5 0,0 1,0.5 12L0.5,4A3.5,3.5 0,0 1,4 0.5z"
android:fillColor="#ffffff"/>
<path
android:strokeWidth="1"
android:pathData="M4,0.5L12,0.5A3.5,3.5 0,0 1,15.5 4L15.5,12A3.5,3.5 0,0 1,12 15.5L4,15.5A3.5,3.5 0,0 1,0.5 12L0.5,4A3.5,3.5 0,0 1,4 0.5z"
android:fillColor="#00000000"
android:strokeColor="#C0BFB1"/>
</vector>

View File

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M4,0.5L12,0.5A3.5,3.5 0,0 1,15.5 4L15.5,12A3.5,3.5 0,0 1,12 15.5L4,15.5A3.5,3.5 0,0 1,0.5 12L0.5,4A3.5,3.5 0,0 1,4 0.5z"
android:fillColor="#231F20"/>
<path
android:strokeWidth="1"
android:pathData="M4,0.5L12,0.5A3.5,3.5 0,0 1,15.5 4L15.5,12A3.5,3.5 0,0 1,12 15.5L4,15.5A3.5,3.5 0,0 1,0.5 12L0.5,4A3.5,3.5 0,0 1,4 0.5z"
android:fillColor="#00000000"
android:strokeColor="#231F20"/>
<path
android:pathData="M12,5L6.5,10.5L4,8"
android:strokeLineJoin="round"
android:strokeWidth="1.6666"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -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",

View File

@ -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()
}

View File

@ -30,7 +30,6 @@ class AboutViewTestSetup(
snackbarHostState = SnackbarHostState(),
topAppBarSubTitleState = TopAppBarSubTitleState.None,
versionInfo = versionInfo,
onWhatsNew = {}
)
}
}

View File

@ -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

View File

@ -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()
}
}
}

View File

@ -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())
}
}

View File

@ -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()
)
}
}
}
}
}
}

View File

@ -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)

View File

@ -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())
}
}

View File

@ -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(),
)
}
}
}
}
}
}

View File

@ -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()
}
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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<String>(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()
}
}
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
)
}

View File

@ -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 = {
WrapSeed(
args = SeedNavigationArgs.RECOVERY,
goBackOverride = {
setSeedRecoveryAuthentication(false)
navController.popBackStackJustOnce(SEED_RECOVERY)
},
onDone = {
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) {

View File

@ -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<Boolean>,
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)
}

View File

@ -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
)
}
}
}

View File

@ -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(

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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<PersistableWallet?> =
walletRepository
.secretState.map { (it as? SecretState.NeedsBackup)?.persistableWallet }
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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<WalletViewModel>()
val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value
@ -64,7 +60,6 @@ internal fun WrapAbout(
},
snackbarHostState = snackbarHostState,
topAppBarSubTitleState = walletState,
onWhatsNew = goWhatsNew
)
}

View File

@ -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)
Text(
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.spacingSmall))
Text(
modifier = Modifier.fillMaxWidth(),
text =
stringResource(
R.string.about_version_format,
versionInfo.versionName
),
textAlign = TextAlign.Center,
style = ZcashTheme.typography.primary.titleSmall
)
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(),
)
}

View File

@ -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)
}
)
}

View File

@ -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()
)
}
}

View File

@ -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 = {})
}

View File

@ -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"
}

View File

@ -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)
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.spacingLarge))
Body(
modifier = Modifier.testTag(ExportPrivateDataScreenTag.WARNING_TEXT_TAG),
text = stringResource(R.string.export_data_text_1)
)
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,
)
}

View File

@ -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<WalletViewModel>()
val viewModel = koinViewModel<FeedbackViewModel>()
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
)
}
}

View File

@ -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 = "😍"
)
}

View File

@ -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

View File

@ -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<Unit>()
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)
}
}

View File

@ -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
)
}

View File

@ -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"
}

View File

@ -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
)
)
}
}
}
}

View File

@ -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 =

View File

@ -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
)

View File

@ -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
)

View File

@ -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 =

View File

@ -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()
}
}

View File

@ -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<WalletViewModel>()
val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value
val viewModel = koinViewModel<SeedViewModel> { 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
}

View File

@ -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,
)

View File

@ -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 = {}
)
)
}

View File

@ -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<Unit>()
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 }
}
}
}

View File

@ -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<WalletViewModel>()
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,
)
}
}

View File

@ -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"
}

View File

@ -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
)
)
}
}
}
}

View File

@ -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<Boolean?> =
flow<Boolean?> {
emitAll(default.observe(standardPreferenceProvider()))

View File

@ -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<SupportViewModel>()
val walletViewModel = koinActivityViewModel<WalletViewModel>()
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,
)
}

View File

@ -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) {

View File

@ -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)
)
)
}

View File

@ -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<WhatsNewSectionState>
) {
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)
)
},
)
}
}

View File

@ -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 = {}
)

View File

@ -15,7 +15,12 @@ import kotlinx.coroutines.flow.stateIn
class WhatsNewViewModel(application: Application) : AndroidViewModel(application) {
val state: StateFlow<WhatsNewState?> =
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)
}

View File

@ -1,14 +1,15 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="about_title">Acerca de</string>
<string name="about_subtitle">Introducing Zashi</string>
<string name="about_version_format" formatted="true">Versión de Zashi <xliff:g example="1" id="version">%s</xliff:g></string>
<string name="about_debug_menu_app_name">Nombre de la app:<xliff:g example="Zashi" id="app_name">%1$s</xliff:g></string>
<string name="about_debug_menu_build">Compilación: <xliff:g example="635dac0eb9ddc2bc6da5177f0dd495d8b76af4dc" id="git_commit_hash">%1$s</xliff:g></string>
<string name="about_description">¡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.</string>
<string name="about_description">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.
</string>
<string name="about_pp_part_1">Consulta nuestra Política de Privacidad\u0020</string>
<string name="about_pp_part_2">aquí</string>
<string name="about_pp_part_3">.</string>
<string name="about_unable_to_web_browser">No se pudo encontrar una aplicación de navegador web.</string>
<string name="about_button_whats_new">Novedades</string>
<string name="about_button_privacy_policy">Política de Privacidad</string>
</resources>

View File

@ -1,14 +1,15 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="about_title">About</string>
<string name="about_subtitle">Introducing Zashi</string>
<string name="about_version_format" formatted="true">Zashi Version <xliff:g example="1" id="version">%s</xliff:g></string>
<string name="about_debug_menu_app_name">App name:<xliff:g example="Zashi" id="app_name">%1$s</xliff:g></string>
<string name="about_debug_menu_build">Build: <xliff:g example="635dac0eb9ddc2bc6da5177f0dd495d8b76af4dc" id="git_commit_hash">%1$s</xliff:g></string>
<string name="about_description">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.</string>
<string name="about_description">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.
</string>
<string name="about_pp_part_1">See our Privacy Policy\u0020</string>
<string name="about_pp_part_2">here</string>
<string name="about_pp_part_3">.</string>
<string name="about_unable_to_web_browser">Unable to find a web browser app.</string>
<string name="about_button_whats_new">What\'s new</string>
<string name="about_button_privacy_policy">Privacy Policy</string>
</resources>

View File

@ -5,5 +5,5 @@
<string name="advanced_settings_choose_server">Elegir un servidor</string>
<string name="advanced_settings_currency_conversion">Conversión de moneda</string>
<string name="advanced_settings_info">Se te pedirá confirmación en la siguiente pantalla</string>
<string name="advanced_settings_delete_button">Eliminar Zashi</string>
<string name="advanced_settings_delete_button">Reset Zashi</string>
</resources>

View File

@ -5,5 +5,5 @@
<string name="advanced_settings_choose_server">Choose a Server</string>
<string name="advanced_settings_currency_conversion">Currency Conversion</string>
<string name="advanced_settings_info">You will be asked to confirm on the next screen</string>
<string name="advanced_settings_delete_button">Delete Zashi</string>
<string name="advanced_settings_delete_button">Reset Zashi</string>
</resources>

View File

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

View File

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<group>
<clip-path
android:pathData="M0,0h16v16h-16z"/>
<path
android:pathData="M8,10.666V8M8,5.333H8.007M14.667,8C14.667,11.682 11.682,14.666 8,14.666C4.318,14.666 1.334,11.682 1.334,8C1.334,4.318 4.318,1.333 8,1.333C11.682,1.333 14.667,4.318 14.667,8Z"
android:strokeLineJoin="round"
android:strokeWidth="1.33"
android:fillColor="#00000000"
android:strokeColor="#A6A391"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@ -1,23 +1,12 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="delete_wallet_title">
Eliminar <xliff:g id="app_name" example="Zashi">%1$s</xliff:g>
</string>
<string name="delete_wallet_title">Reset Zashi</string>
<string name="delete_wallet_text_1">
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.
</string>
<string name="delete_wallet_text_2">
Eliminar la aplicación <xliff:g id="app_name" example="Zashi">%1$s</xliff:g> 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 <xliff:g id="app_name" example="Zashi">%1$s</xliff:g> en <xliff:g id="app_name"
example="Zashi">%1$s</xliff:g> u otra billetera Zcash.
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.
</string>
<string name="delete_wallet_acknowledge">Entiendo</string>
<string name="delete_wallet_button">
Eliminar <xliff:g id="app_name" example="Zashi">%1$s</xliff:g>
</string>
<string name="delete_wallet_failed">La eliminación de la billetera falló. Inténtelo de nuevo, por favor.</string>
<string name="delete_wallet_acknowledge">I understand</string>
<string name="delete_wallet_button">Reset Zashi</string>
<string name="delete_wallet_failed">Wallet deletion failed. Try it again, please.</string>
</resources>

View File

@ -1,23 +1,12 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="delete_wallet_title">
Delete <xliff:g id="app_name" example="Zashi">%1$s</xliff:g>
</string>
<string name="delete_wallet_title">Reset Zashi</string>
<string name="delete_wallet_text_1">
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.
</string>
<string name="delete_wallet_text_2">
Deleting the <xliff:g id="app_name" example="Zashi">%1$s</xliff:g> 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 <xliff:g
id="app_name" example="Zashi">%1$s</xliff:g> secret recovery phrase in <xliff:g id="app_name"
example="Zashi">%1$s</xliff:g> 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.
</string>
<string name="delete_wallet_acknowledge">I understand</string>
<string name="delete_wallet_button">
Delete <xliff:g id="app_name" example="Zashi">%1$s</xliff:g>
</string>
<string name="delete_wallet_button">Reset Zashi</string>
<string name="delete_wallet_failed">Wallet deletion failed. Try it again, please.</string>
</resources>

View File

@ -1,9 +1,16 @@
<resources>
<string name="export_data_title">Data Export</string>
<string name="export_data_header">Consentimiento para Exportar Datos Privados</string>
<string name="export_data_text_1">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.</string>
<string name="export_data_text_2">*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.</string>
<string name="export_data_confirm">Exportar datos privados</string>
<string name="export_data_agree">Estoy de acuerdo</string>
<string name="export_data_text">
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.
</string>
<string name="export_data_confirm">Export Private Data</string>
<string name="export_data_agree">I agree to Zashi\'s Export Private Data Policies and Privacy Policy</string>
<string name="export_data_export_data_chooser_title">Compartir datos internos de Zashi con:</string>
<string name="export_data_unable_to_share">No se pudo encontrar una aplicación con la cual compartir.</string>
</resources>

View File

@ -1,14 +1,16 @@
<resources>
<string name="export_data_title">Data Export</string>
<string name="export_data_header">Consent for Exporting Private Data</string>
<string name="export_data_text_1">By clicking \"I Agree\" below, you give your consent to export Zashis 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.</string>
<string name="export_data_text_2">*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.</string>
<string name="export_data_confirm">Export private data</string>
<string name="export_data_agree">I agree</string>
<string name="export_data_text">
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.
</string>
<string name="export_data_confirm">Export Private Data</string>
<string name="export_data_agree">I agree to Zashi\'s Export Private Data Policies and Privacy Policy</string>
<string name="export_data_export_data_chooser_title">Share internal Zashi data with:</string>
<string name="export_data_unable_to_share">Unable to find an application to share with.</string>
</resources>

View File

@ -0,0 +1,37 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="106dp"
android:height="64dp"
android:viewportWidth="106"
android:viewportHeight="64">
<path
android:pathData="M8,30C8,16.75 18.75,6 32,6C45.25,6 56,16.75 56,30C56,43.25 45.25,54 32,54C18.75,54 8,43.25 8,30Z"
android:fillColor="#454243"/>
<path
android:pathData="M32,4C17.64,4 6,15.64 6,30C6,44.36 17.64,56 32,56C46.36,56 58,44.36 58,30C58,15.64 46.36,4 32,4Z"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#231F20"/>
<path
android:pathData="M26.09,29.23C26.03,28.83 26,28.42 26,28C26,23.58 29.61,20 34.05,20C38.5,20 42.11,23.58 42.11,28C42.11,29 41.92,29.95 41.59,30.83C41.52,31.02 41.48,31.11 41.46,31.18C41.45,31.25 41.44,31.3 41.44,31.37C41.44,31.45 41.45,31.53 41.47,31.69L41.87,34.96C41.92,35.31 41.94,35.49 41.88,35.62C41.83,35.73 41.74,35.82 41.62,35.87C41.49,35.93 41.31,35.9 40.96,35.85L37.78,35.38C37.61,35.36 37.53,35.34 37.45,35.34C37.38,35.35 37.32,35.35 37.25,35.37C37.18,35.38 37.08,35.42 36.89,35.49C36.01,35.82 35.05,36 34.05,36C33.63,36 33.22,35.97 32.82,35.91M27.63,40C30.6,40 33,37.54 33,34.5C33,31.46 30.6,29 27.63,29C24.67,29 22.26,31.46 22.26,34.5C22.26,35.11 22.36,35.7 22.54,36.25C22.62,36.48 22.65,36.59 22.67,36.67C22.68,36.76 22.68,36.8 22.68,36.89C22.67,36.97 22.65,37.06 22.61,37.24L22,40L24.99,39.59C25.16,39.57 25.24,39.56 25.31,39.56C25.39,39.56 25.43,39.56 25.5,39.58C25.57,39.59 25.67,39.63 25.88,39.7C26.43,39.89 27.02,40 27.63,40Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"
android:strokeLineCap="round"/>
<group>
<clip-path
android:pathData="M52,30C52,16.75 62.75,6 76,6C89.25,6 100,16.75 100,30C100,43.25 89.25,54 76,54C62.75,54 52,43.25 52,30Z"/>
<path
android:pathData="M76,6L76,6A24,24 0,0 1,100 30L100,30A24,24 0,0 1,76 54L76,54A24,24 0,0 1,52 30L52,30A24,24 0,0 1,76 6z"
android:fillColor="#231F20"/>
<path
android:pathData="M52,30C52,16.76 62.76,6 76,6C89.24,6 100,16.76 100,30C100,43.24 89.24,54 76,54C62.76,54 52,43.24 52,30ZM84.56,18.86V22.51L74.4,36.29H84.56V41.14H78.01V45.15H73.99V41.14H67.44V37.48L77.59,23.71H67.44V18.86H73.99V14.84H78.01V18.86H84.56Z"
android:fillColor="#FCBB1A"
android:fillType="evenOdd"/>
</group>
<path
android:pathData="M76,4.5C61.92,4.5 50.5,15.92 50.5,30C50.5,44.08 61.92,55.5 76,55.5C90.08,55.5 101.5,44.08 101.5,30C101.5,15.92 90.08,4.5 76,4.5Z"
android:strokeWidth="3"
android:fillColor="#00000000"
android:strokeColor="#231F20"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,37 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="106dp"
android:height="64dp"
android:viewportWidth="106"
android:viewportHeight="64">
<path
android:pathData="M8,30C8,16.75 18.75,6 32,6C45.25,6 56,16.75 56,30C56,43.25 45.25,54 32,54C18.75,54 8,43.25 8,30Z"
android:fillColor="#EBEBE6"/>
<path
android:pathData="M32,4C17.64,4 6,15.64 6,30C6,44.36 17.64,56 32,56C46.36,56 58,44.36 58,30C58,15.64 46.36,4 32,4Z"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#ffffff"/>
<path
android:pathData="M26.09,29.23C26.03,28.83 26,28.42 26,28C26,23.58 29.61,20 34.05,20C38.5,20 42.11,23.58 42.11,28C42.11,29 41.92,29.95 41.59,30.83C41.52,31.02 41.48,31.11 41.46,31.18C41.45,31.25 41.44,31.3 41.44,31.37C41.44,31.45 41.45,31.53 41.47,31.69L41.87,34.96C41.92,35.31 41.94,35.49 41.88,35.62C41.83,35.73 41.74,35.82 41.62,35.87C41.49,35.93 41.31,35.9 40.96,35.85L37.78,35.38C37.61,35.36 37.53,35.34 37.45,35.34C37.38,35.35 37.32,35.35 37.25,35.37C37.18,35.38 37.08,35.42 36.89,35.49C36.01,35.82 35.05,36 34.05,36C33.63,36 33.22,35.97 32.82,35.91M27.63,40C30.6,40 33,37.54 33,34.5C33,31.46 30.6,29 27.63,29C24.67,29 22.26,31.46 22.26,34.5C22.26,35.11 22.36,35.7 22.54,36.25C22.62,36.48 22.65,36.59 22.67,36.67C22.68,36.76 22.68,36.8 22.68,36.89C22.67,36.97 22.65,37.06 22.61,37.24L22,40L24.99,39.59C25.16,39.57 25.24,39.56 25.31,39.56C25.39,39.56 25.43,39.56 25.5,39.58C25.57,39.59 25.67,39.63 25.88,39.7C26.43,39.89 27.02,40 27.63,40Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
<group>
<clip-path
android:pathData="M52,30C52,16.75 62.75,6 76,6C89.25,6 100,16.75 100,30C100,43.25 89.25,54 76,54C62.75,54 52,43.25 52,30Z"/>
<path
android:pathData="M76,6L76,6A24,24 0,0 1,100 30L100,30A24,24 0,0 1,76 54L76,54A24,24 0,0 1,52 30L52,30A24,24 0,0 1,76 6z"
android:fillColor="#ffffff"/>
<path
android:pathData="M52,30C52,16.76 62.76,6 76,6C89.24,6 100,16.76 100,30C100,43.24 89.24,54 76,54C62.76,54 52,43.24 52,30ZM84.56,18.86V22.51L74.4,36.29H84.56V41.14H78.01V45.15H73.99V41.14H67.44V37.48L77.59,23.71H67.44V18.86H73.99V14.84H78.01V18.86H84.56Z"
android:fillColor="#FCBB1A"
android:fillType="evenOdd"/>
</group>
<path
android:pathData="M76,4.5C61.92,4.5 50.5,15.92 50.5,30C50.5,44.08 61.92,55.5 76,55.5C90.08,55.5 101.5,44.08 101.5,30C101.5,15.92 90.08,4.5 76,4.5Z"
android:strokeWidth="3"
android:fillColor="#00000000"
android:strokeColor="#ffffff"/>
</vector>

View File

@ -1,12 +1,16 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="support_header">Support</string>
<string name="support_hint">How can we help?</string>
<string name="support_title">Send Us Feedback</string>
<string name="support_experience_title">How is your Zashi experience?</string>
<string name="support_help_title">How can we help you?</string>
<string name="support_hint">I would like to ask about…</string>
<string name="support_send">Send</string>
<string name="support_confirmation_dialog_ok">OK</string>
<string name="support_confirmation_dialog_cancel">Cancel</string>
<string name="support_confirmation_dialog_title">Open e-mail app</string>
<string name="support_confirmation_explanation"><xliff:g id="app_name" example="Zcash">%1$s</xliff:g> is about to
<string name="support_confirmation_explanation">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.</string>
<string name="support_information">Please let us know about any problems you have had, or features you want to see in the future.</string>
<string name="support_email_part_1">How is your Zashi experience?\n%s %s/5</string>
<string name="support_email_part_2">How can we help you?\n%s</string>
</resources>

View File

@ -0,0 +1,16 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="support_header">Support</string>
<string name="support_title">Send Us Feedback</string>
<string name="support_experience_title">How is your Zashi experience?</string>
<string name="support_help_title">How can we help you?</string>
<string name="support_hint">I would like to ask about…</string>
<string name="support_send">Send</string>
<string name="support_confirmation_dialog_ok">OK</string>
<string name="support_confirmation_dialog_cancel">Cancel</string>
<string name="support_confirmation_dialog_title">Open e-mail app</string>
<string name="support_confirmation_explanation">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.</string>
<string name="support_information">Please let us know about any problems you have had, or features you want to see in the future.</string>
<string name="support_email_part_1">How is your Zashi experience?\n%s %s/5</string>
<string name="support_email_part_2">How can we help you?\n%s</string>
</resources>

View File

@ -1,9 +0,0 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="new_wallet_recovery_header">Tu frase secreta de recuperación</string>
<string name="new_wallet_recovery_description">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!</string>
<string name="new_wallet_recovery_birthday_height" formatted="true">Altura de cumpleaños de la billetera: <xliff:g example="419200" id="birthday_height">%1$d</xliff:g></string>
<string name="new_wallet_recovery_button_finished">Ya la he guardado</string>
<string name="new_wallet_recovery_copy">Toca para copiar</string>
<string name="new_wallet_recovery_seed_clipboard_tag">Frase de Semilla de Zcash</string>
<string name="new_wallet_recovery_birthday_clipboard_tag">Cumpleaños de la Billetera Zcash</string>
</resources>

View File

@ -1,12 +0,0 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="new_wallet_recovery_header">Your secret recovery phrase</string>
<string name="new_wallet_recovery_description">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!</string>
<string name="new_wallet_recovery_birthday_height" formatted="true">Wallet birthday height: <xliff:g example="419200"
id="birthday_height">%1$d</xliff:g></string>
<string name="new_wallet_recovery_button_finished">I\'ve saved it</string>
<string name="new_wallet_recovery_copy">Tap to Copy</string>
<string name="new_wallet_recovery_seed_clipboard_tag">Zcash Seed Phrase</string>
<string name="new_wallet_recovery_birthday_clipboard_tag">Zcash Wallet Birthday</string>
</resources>

View File

@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="33dp"
android:height="32dp"
android:viewportWidth="33"
android:viewportHeight="32">
<path
android:pathData="M16.5,20C18.709,20 20.5,18.209 20.5,16C20.5,13.791 18.709,12 16.5,12C14.291,12 12.5,13.791 12.5,16C12.5,18.209 14.291,20 16.5,20Z"
android:strokeAlpha="0.12"
android:fillColor="#231F20"
android:fillAlpha="0.12"/>
<path
android:pathData="M3.726,16.951C3.545,16.664 3.454,16.52 3.403,16.298C3.365,16.132 3.365,15.869 3.403,15.702C3.454,15.481 3.545,15.337 3.726,15.049C5.227,12.673 9.693,6.667 16.5,6.667C23.307,6.667 27.773,12.673 29.274,15.049C29.455,15.337 29.546,15.481 29.597,15.702C29.635,15.869 29.635,16.132 29.597,16.298C29.546,16.52 29.455,16.664 29.274,16.951C27.773,19.327 23.307,25.334 16.5,25.334C9.693,25.334 5.227,19.327 3.726,16.951Z"
android:strokeLineJoin="round"
android:strokeWidth="2.66667"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
<path
android:pathData="M16.5,20C18.709,20 20.5,18.209 20.5,16C20.5,13.791 18.709,12 16.5,12C14.291,12 12.5,13.791 12.5,16C12.5,18.209 14.291,20 16.5,20Z"
android:strokeLineJoin="round"
android:strokeWidth="2.66667"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="18dp"
android:viewportWidth="20"
android:viewportHeight="18">
<path
android:pathData="M8.952,3.244C9.291,3.194 9.641,3.167 10,3.167C14.255,3.167 17.046,6.921 17.984,8.406C18.097,8.585 18.154,8.675 18.186,8.814C18.21,8.918 18.21,9.082 18.186,9.186C18.154,9.325 18.097,9.415 17.983,9.596C17.733,9.992 17.352,10.548 16.847,11.15M5.604,4.596C3.802,5.818 2.579,7.516 2.018,8.404C1.904,8.585 1.847,8.675 1.815,8.814C1.791,8.918 1.791,9.082 1.815,9.186C1.847,9.325 1.903,9.415 2.017,9.594C2.955,11.079 5.746,14.833 10,14.833C11.716,14.833 13.193,14.223 14.407,13.397M2.5,1.5L17.5,16.5M8.233,7.232C7.78,7.685 7.5,8.31 7.5,9C7.5,10.381 8.62,11.5 10,11.5C10.691,11.5 11.316,11.22 11.768,10.768"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="14dp"
android:viewportWidth="20"
android:viewportHeight="14">
<path
android:pathData="M2.017,7.595C1.903,7.415 1.847,7.325 1.815,7.187C1.791,7.082 1.791,6.918 1.815,6.814C1.847,6.676 1.903,6.586 2.017,6.406C2.955,4.921 5.746,1.167 10,1.167C14.255,1.167 17.046,4.921 17.984,6.406C18.097,6.586 18.154,6.676 18.186,6.814C18.21,6.918 18.21,7.082 18.186,7.187C18.154,7.325 18.097,7.415 17.984,7.595C17.046,9.08 14.255,12.834 10,12.834C5.746,12.834 2.955,9.08 2.017,7.595Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
<path
android:pathData="M10,9.5C11.381,9.5 12.5,8.381 12.5,7C12.5,5.62 11.381,4.5 10,4.5C8.62,4.5 7.5,5.62 7.5,7C7.5,8.381 8.62,9.5 10,9.5Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</vector>

View File

@ -1,9 +1,18 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="seed_recovery_header">Tu frase secreta de recuperación</string>
<string name="seed_recovery_description">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!</string>
<string name="seed_recovery_birthday_height" formatted="true">Altura de cumpleaños de la billetera: <xliff:g example="419200" id="birthday_height">%1$d</xliff:g></string>
<string name="seed_recovery_button_finished">¡Entendido!</string>
<string name="seed_recovery_copy">Toca para copiar</string>
<string name="seed_recovery_seed_clipboard_tag">Frase de Semilla de Zcash</string>
<string name="seed_recovery_birthday_clipboard_tag">Cumpleaños de la Billetera Zcash</string>
<string name="seed_recovery_title">Recovery Phrase</string>
<string name="seed_recovery_header">Secure Your Wallet</string>
<string name="seed_recovery_description">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.</string>
<string name="seed_recovery_reveal_button">Reveal security details</string>
<string name="seed_recovery_hide_button">Hide security details</string>
<string name="seed_recovery_next_button">Next</string>
<string name="seed_recovery_phrase_title">Recovery Phrase</string>
<string name="seed_recovery_bday_title">Wallet Birthday Height</string>
<string name="seed_recovery_bday_tooltip_title">Wallet Birthday Height</string>
<string name="seed_recovery_bday_tooltip_message">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.</string>
<string name="seed_recovery_warning">Protect your ZEC by storing this phrase in a place you trust and never share
it with anyone!</string>
<string name="seed_recovery_reveal">Reveal recovery phrase</string>
</resources>

View File

@ -1,12 +1,18 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="seed_recovery_header">Your secret recovery phrase</string>
<string name="seed_recovery_description">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!</string>
<string name="seed_recovery_birthday_height" formatted="true">Wallet birthday height: <xliff:g example="419200"
id="birthday_height">%1$d</xliff:g></string>
<string name="seed_recovery_button_finished">I got it!</string>
<string name="seed_recovery_copy">Tap to Copy</string>
<string name="seed_recovery_seed_clipboard_tag">Zcash Seed Phrase</string>
<string name="seed_recovery_birthday_clipboard_tag">Zcash Wallet Birthday</string>
<string name="seed_recovery_title">Recovery Phrase</string>
<string name="seed_recovery_header">Secure Your Wallet</string>
<string name="seed_recovery_description">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.</string>
<string name="seed_recovery_reveal_button">Reveal security details</string>
<string name="seed_recovery_hide_button">Hide security details</string>
<string name="seed_recovery_next_button">Next</string>
<string name="seed_recovery_phrase_title">Recovery Phrase</string>
<string name="seed_recovery_bday_title">Wallet Birthday Height</string>
<string name="seed_recovery_bday_tooltip_title">Wallet Birthday Height</string>
<string name="seed_recovery_bday_tooltip_message">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.</string>
<string name="seed_recovery_warning">Protect your ZEC by storing this phrase in a place you trust and never share
it with anyone!</string>
<string name="seed_recovery_reveal">Reveal recovery phrase</string>
</resources>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M0,20C0,8.954 8.954,0 20,0C31.046,0 40,8.954 40,20C40,31.046 31.046,40 20,40C8.954,40 0,31.046 0,20Z"
android:fillColor="#454243"/>
<path
android:pathData="M22.5,13.333V11.667M22.5,23.333V21.667M16.667,17.5H18.333M26.667,17.5H28.333M24.833,19.833L25.833,20.833M24.833,15.167L25.833,14.167M12.5,27.5L20,20M20.167,15.167L19.167,14.167"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M0,20C0,8.954 8.954,0 20,0C31.046,0 40,8.954 40,20C40,31.046 31.046,40 20,40C8.954,40 0,31.046 0,20Z"
android:fillColor="#EBEBE6"/>
<path
android:pathData="M22.5,13.333V11.667M22.5,23.333V21.667M16.667,17.5H18.333M26.667,17.5H28.333M24.833,19.833L25.833,20.833M24.833,15.167L25.833,14.167M12.5,27.5L20,20M20.167,15.167L19.167,14.167"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</vector>

Some files were not shown because too many files have changed in this diff Show More