[#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 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 - The Not enough space and In-app udpate screens have been redesigned
- External links now open in in-app browser - External links now open in in-app browser
- All the Settings screens have been redesigned
### Fixed ### Fixed
- Address book toast now correctly shows on send screen when adding both new and known addresses to text field - 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 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 - The Not enough space and In-app udpate screens have been redesigned
- External links now open in in-app browser - External links now open in in-app browser
- All the Settings screens have been redesigned
### Fixed ### Fixed
- Address book toast now correctly shows on send screen when adding both new and known addresses to text field - 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 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 - The Not enough space and In-app udpate screens have been redesigned
- External links now open in in-app browser - External links now open in in-app browser
- All the Settings screens have been redesigned
### Fixed ### Fixed
- Address book toast now correctly shows on send screen when adding both new and known addresses to text field - 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: ignoreAnnotated:
- 'Preview' - 'Preview'
- 'PreviewScreens' - 'PreviewScreens'
- 'PreviewScreenSizes'
MagicNumber:
active: true
ignoreAnnotated:
- 'Preview'
- 'PreviewScreens'
- 'PreviewScreenSizes'
complexity: complexity:
LongMethod: LongMethod:
@ -39,11 +46,13 @@ complexity:
ignoreAnnotated: ignoreAnnotated:
- 'Preview' - 'Preview'
- 'PreviewScreens' - 'PreviewScreens'
- 'PreviewScreenSizes'
LongParameterList: LongParameterList:
active: false active: false
ignoreAnnotated: ignoreAnnotated:
- 'Preview' - 'Preview'
- 'PreviewScreens' - 'PreviewScreens'
- 'PreviewScreenSizes'
Compose: Compose:
ModifierMissing: 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 package co.electriccoin.zcash.ui.design.component
import androidx.annotation.DrawableRes
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
@ -13,12 +14,13 @@ import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color 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.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -39,7 +41,7 @@ fun ZashiButton(
) { ) {
ZashiButton( ZashiButton(
text = state.text.getValue(), text = state.text.getValue(),
leadingIcon = state.leadingIconVector, icon = state.icon,
onClick = state.onClick, onClick = state.onClick,
modifier = modifier, modifier = modifier,
enabled = state.isEnabled, enabled = state.isEnabled,
@ -55,7 +57,7 @@ fun ZashiButton(
text: String, text: String,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
leadingIcon: Painter? = null, @DrawableRes icon: Int? = null,
enabled: Boolean = true, enabled: Boolean = true,
isLoading: Boolean = false, isLoading: Boolean = false,
colors: ZashiButtonColors = ZashiButtonDefaults.primaryColors(), colors: ZashiButtonColors = ZashiButtonDefaults.primaryColors(),
@ -65,11 +67,12 @@ fun ZashiButton(
object : ZashiButtonScope { object : ZashiButtonScope {
@Composable @Composable
override fun LeadingIcon() { override fun LeadingIcon() {
if (leadingIcon != null) { if (icon != null) {
Image( Image(
painter = leadingIcon, painter = painterResource(icon),
contentDescription = null, 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( Button(
onClick = onClick, onClick = onClick,
modifier = modifier, modifier = modifier,
@ -105,7 +110,7 @@ fun ZashiButton(
contentPadding = PaddingValues(horizontal = 10.dp), contentPadding = PaddingValues(horizontal = 10.dp),
enabled = enabled, enabled = enabled,
colors = colors.toButtonColors(), 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 = {
content(scope) content(scope)
} }
@ -142,9 +147,10 @@ object ZashiButtonDefaults {
) = ZashiButtonColors( ) = ZashiButtonColors(
containerColor = containerColor, containerColor = containerColor,
contentColor = contentColor, contentColor = contentColor,
borderColor = Color.Unspecified,
disabledContainerColor = disabledContainerColor, disabledContainerColor = disabledContainerColor,
disabledContentColor = disabledContentColor, disabledContentColor = disabledContentColor,
borderColor = Color.Unspecified disabledBorderColor = Color.Unspecified
) )
@Composable @Composable
@ -156,9 +162,10 @@ object ZashiButtonDefaults {
) = ZashiButtonColors( ) = ZashiButtonColors(
containerColor = containerColor, containerColor = containerColor,
contentColor = contentColor, contentColor = contentColor,
borderColor = Color.Unspecified,
disabledContainerColor = disabledContainerColor, disabledContainerColor = disabledContainerColor,
disabledContentColor = disabledContentColor, disabledContentColor = disabledContentColor,
borderColor = Color.Unspecified disabledBorderColor = Color.Unspecified
) )
@Composable @Composable
@ -170,9 +177,10 @@ object ZashiButtonDefaults {
) = ZashiButtonColors( ) = ZashiButtonColors(
containerColor = containerColor, containerColor = containerColor,
contentColor = contentColor, contentColor = contentColor,
borderColor = Color.Unspecified,
disabledContainerColor = disabledContainerColor, disabledContainerColor = disabledContainerColor,
disabledContentColor = disabledContentColor, disabledContentColor = disabledContentColor,
borderColor = Color.Unspecified disabledBorderColor = Color.Unspecified
) )
@Composable @Composable
@ -187,7 +195,8 @@ object ZashiButtonDefaults {
contentColor = contentColor, contentColor = contentColor,
disabledContainerColor = disabledContainerColor, disabledContainerColor = disabledContainerColor,
disabledContentColor = disabledContentColor, disabledContentColor = disabledContentColor,
borderColor = borderColor borderColor = borderColor,
disabledBorderColor = Color.Unspecified
) )
} }
@ -195,15 +204,16 @@ object ZashiButtonDefaults {
data class ZashiButtonColors( data class ZashiButtonColors(
val containerColor: Color, val containerColor: Color,
val contentColor: Color, val contentColor: Color,
val borderColor: Color,
val disabledContainerColor: Color, val disabledContainerColor: Color,
val disabledContentColor: Color, val disabledContentColor: Color,
val borderColor: Color, val disabledBorderColor: Color,
) )
@Immutable @Immutable
data class ButtonState( data class ButtonState(
val text: StringResource, val text: StringResource,
val leadingIconVector: Painter? = null, @DrawableRes val icon: Int? = null,
val isEnabled: Boolean = true, val isEnabled: Boolean = true,
val isLoading: Boolean = false, val isLoading: Boolean = false,
val onClick: () -> Unit = {}, val onClick: () -> Unit = {},
@ -239,7 +249,7 @@ private fun PrimaryWithIconPreview() =
ZashiButton( ZashiButton(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = "Primary", text = "Primary",
leadingIcon = painterResource(id = android.R.drawable.ic_secure), icon = android.R.drawable.ic_secure,
onClick = {}, 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 package co.electriccoin.zcash.ui.design.newcomponent
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import kotlin.annotation.AnnotationRetention.SOURCE 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) @Preview(name = "2: Dark preview", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Retention(SOURCE) @Retention(SOURCE)
annotation class PreviewScreens 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.foundation.layout.padding
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions
@Stable @Stable
@ -14,3 +15,11 @@ fun Modifier.scaffoldPadding(paddingValues: PaddingValues) =
start = ZashiDimensions.Spacing.spacing3xl, start = ZashiDimensions.Spacing.spacing3xl,
end = 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/home",
"src/main/res/ui/choose_server", "src/main/res/ui/choose_server",
"src/main/res/ui/integrations", "src/main/res/ui/integrations",
"src/main/res/ui/new_wallet_recovery",
"src/main/res/ui/onboarding", "src/main/res/ui/onboarding",
"src/main/res/ui/payment_request", "src/main/res/ui/payment_request",
"src/main/res/ui/qr_code", "src/main/res/ui/qr_code",
@ -62,7 +61,7 @@ android {
"src/main/res/ui/send", "src/main/res/ui/send",
"src/main/res/ui/send_confirmation", "src/main/res/ui/send_confirmation",
"src/main/res/ui/settings", "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",
"src/main/res/ui/update_contact", "src/main/res/ui/update_contact",
"src/main/res/ui/wallet_address", "src/main/res/ui/wallet_address",

View File

@ -27,20 +27,14 @@ class AboutViewTest {
assertEquals(0, testSetup.getOnBackCount()) assertEquals(0, testSetup.getOnBackCount())
composeTestRule composeTestRule
.onNodeWithText( .onNodeWithContentDescription(
getStringResource(R.string.back_navigation), getStringResource(R.string.back_navigation_content_description),
ignoreCase = true ignoreCase = true
) )
.also { .also {
it.assertExists() it.assertExists()
} }
composeTestRule.onNodeWithContentDescription(
label = getStringResource(R.string.zcash_logo_content_description)
).also {
it.assertExists()
}
composeTestRule.onNodeWithText(getStringResource(R.string.about_description)).also { composeTestRule.onNodeWithText(getStringResource(R.string.about_description)).also {
it.assertExists() it.assertExists()
} }

View File

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

View File

@ -53,12 +53,6 @@ class ExportPrivateDataViewTest : UiTestPrerequisites() {
it.assertExists() it.assertExists()
it.assertIsDisplayed() it.assertIsDisplayed()
} }
composeTestRule.onNodeWithTag(ExportPrivateDataScreenTag.ADDITIONAL_TEXT_TAG).also {
it.performScrollTo()
it.assertExists()
it.assertIsDisplayed()
}
} }
@Test @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 clipboardManager = context.getSystemService(ClipboardManager::class.java)
val data = val data =
ClipData.newPlainText( ClipData.newPlainText(
context.getString(R.string.new_wallet_recovery_seed_clipboard_tag), "TAG",
text text
) )
clipboardManager.setPrimaryClip(data) 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.DeleteAddressBookUseCase
import co.electriccoin.zcash.ui.common.usecase.DeleteContactUseCase import co.electriccoin.zcash.ui.common.usecase.DeleteContactUseCase
import co.electriccoin.zcash.ui.common.usecase.GetAddressesUseCase 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.GetContactByAddressUseCase
import co.electriccoin.zcash.ui.common.usecase.GetPersistableWalletUseCase import co.electriccoin.zcash.ui.common.usecase.GetPersistableWalletUseCase
import co.electriccoin.zcash.ui.common.usecase.GetSelectedEndpointUseCase import co.electriccoin.zcash.ui.common.usecase.GetSelectedEndpointUseCase
import co.electriccoin.zcash.ui.common.usecase.GetSpendingKeyUseCase 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.GetSynchronizerUseCase
import co.electriccoin.zcash.ui.common.usecase.GetTransparentAddressUseCase import co.electriccoin.zcash.ui.common.usecase.GetTransparentAddressUseCase
import co.electriccoin.zcash.ui.common.usecase.IsCoinbaseAvailableUseCase import co.electriccoin.zcash.ui.common.usecase.IsCoinbaseAvailableUseCase
import co.electriccoin.zcash.ui.common.usecase.IsFlexaAvailableUseCase import co.electriccoin.zcash.ui.common.usecase.IsFlexaAvailableUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveAddressBookContactsUseCase 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.ObserveConfigurationUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveContactByAddressUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveContactByAddressUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveContactPickedUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveContactPickedUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveFastestServersUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveFastestServersUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveIsFlexaAvailableUseCase 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.ObserveSelectedEndpointUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveSynchronizerUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveSynchronizerUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveWalletStateUseCase 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.RefreshFastestServersUseCase
import co.electriccoin.zcash.ui.common.usecase.RescanBlockchainUseCase import co.electriccoin.zcash.ui.common.usecase.RescanBlockchainUseCase
import co.electriccoin.zcash.ui.common.usecase.SaveContactUseCase 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.SensitiveSettingsVisibleUseCase
import co.electriccoin.zcash.ui.common.usecase.ShareImageUseCase import co.electriccoin.zcash.ui.common.usecase.ShareImageUseCase
import co.electriccoin.zcash.ui.common.usecase.UpdateContactUseCase 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.Zip321BuildUriUseCase
import co.electriccoin.zcash.ui.common.usecase.Zip321ParseUriValidationUseCase import co.electriccoin.zcash.ui.common.usecase.Zip321ParseUriValidationUseCase
import co.electriccoin.zcash.ui.common.usecase.Zip321ProposalFromUriUseCase 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.core.module.dsl.singleOf
import org.koin.dsl.module import org.koin.dsl.module
val useCaseModule = val useCaseModule =
module { module {
singleOf(::ObserveSynchronizerUseCase) factoryOf(::ObserveSynchronizerUseCase)
singleOf(::GetSynchronizerUseCase) factoryOf(::GetSynchronizerUseCase)
singleOf(::ObserveFastestServersUseCase) factoryOf(::ObserveFastestServersUseCase)
singleOf(::ObserveSelectedEndpointUseCase) factoryOf(::ObserveSelectedEndpointUseCase)
singleOf(::RefreshFastestServersUseCase) factoryOf(::RefreshFastestServersUseCase)
singleOf(::PersistEndpointUseCase) factoryOf(::PersistEndpointUseCase)
singleOf(::ValidateEndpointUseCase) factoryOf(::ValidateEndpointUseCase)
singleOf(::GetPersistableWalletUseCase) factoryOf(::GetPersistableWalletUseCase)
singleOf(::GetSelectedEndpointUseCase) factoryOf(::GetSelectedEndpointUseCase)
singleOf(::ObserveConfigurationUseCase) factoryOf(::ObserveConfigurationUseCase)
singleOf(::RescanBlockchainUseCase) factoryOf(::RescanBlockchainUseCase)
singleOf(::GetTransparentAddressUseCase) factoryOf(::GetTransparentAddressUseCase)
singleOf(::ObserveAddressBookContactsUseCase) factoryOf(::ObserveAddressBookContactsUseCase)
singleOf(::DeleteAddressBookUseCase) factoryOf(::DeleteAddressBookUseCase)
singleOf(::ValidateContactAddressUseCase) factoryOf(::ValidateContactAddressUseCase)
singleOf(::ValidateContactNameUseCase) factoryOf(::ValidateContactNameUseCase)
singleOf(::SaveContactUseCase) factoryOf(::SaveContactUseCase)
singleOf(::UpdateContactUseCase) factoryOf(::UpdateContactUseCase)
singleOf(::DeleteContactUseCase) factoryOf(::DeleteContactUseCase)
singleOf(::GetContactByAddressUseCase) factoryOf(::GetContactByAddressUseCase)
singleOf(::ObserveContactByAddressUseCase) factoryOf(::ObserveContactByAddressUseCase)
singleOf(::ObserveContactPickedUseCase) singleOf(::ObserveContactPickedUseCase)
singleOf(::GetAddressesUseCase) factoryOf(::GetAddressesUseCase)
singleOf(::CopyToClipboardUseCase) factoryOf(::CopyToClipboardUseCase)
singleOf(::IsFlexaAvailableUseCase) factoryOf(::ShareImageUseCase)
singleOf(::ObserveIsFlexaAvailableUseCase) factoryOf(::Zip321BuildUriUseCase)
singleOf(::ShareImageUseCase) factoryOf(::Zip321ProposalFromUriUseCase)
singleOf(::Zip321BuildUriUseCase) factoryOf(::Zip321ParseUriValidationUseCase)
singleOf(::Zip321ProposalFromUriUseCase) factoryOf(::ObserveWalletStateUseCase)
singleOf(::Zip321ParseUriValidationUseCase) factoryOf(::IsCoinbaseAvailableUseCase)
singleOf(::ObserveWalletStateUseCase) factoryOf(::GetSpendingKeyUseCase)
singleOf(::IsCoinbaseAvailableUseCase) factoryOf(::ObservePersistableWalletUseCase)
singleOf(::GetSpendingKeyUseCase) factoryOf(::ObserveBackupPersistableWalletUseCase)
singleOf(::SensitiveSettingsVisibleUseCase) 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.chooseserver.ChooseServerViewModel
import co.electriccoin.zcash.ui.screen.contact.viewmodel.AddContactViewModel import co.electriccoin.zcash.ui.screen.contact.viewmodel.AddContactViewModel
import co.electriccoin.zcash.ui.screen.contact.viewmodel.UpdateContactViewModel 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.integrations.viewmodel.IntegrationsViewModel
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
import co.electriccoin.zcash.ui.screen.paymentrequest.viewmodel.PaymentRequestViewModel 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.restoresuccess.viewmodel.RestoreSuccessViewModel
import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs
import co.electriccoin.zcash.ui.screen.scan.viewmodel.ScanViewModel 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.send.SendViewModel
import co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel.CreateTransactionsViewModel import co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel.CreateTransactionsViewModel
import co.electriccoin.zcash.ui.screen.settings.viewmodel.ScreenBrightnessViewModel import co.electriccoin.zcash.ui.screen.settings.viewmodel.ScreenBrightnessViewModel
@ -89,4 +92,13 @@ val viewModelModule =
} }
viewModelOf(::IntegrationsViewModel) viewModelOf(::IntegrationsViewModel)
viewModelOf(::SendViewModel) 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.WrapAuthentication
import co.electriccoin.zcash.ui.screen.authentication.view.AnimationConstants import co.electriccoin.zcash.ui.screen.authentication.view.AnimationConstants
import co.electriccoin.zcash.ui.screen.authentication.view.WelcomeAnimationAutostart 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.WrapOnboarding
import co.electriccoin.zcash.ui.screen.onboarding.persistExistingWalletWithSeedPhrase import co.electriccoin.zcash.ui.screen.onboarding.persistExistingWalletWithSeedPhrase
import co.electriccoin.zcash.ui.screen.securitywarning.WrapSecurityWarning 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.ui.screen.warning.viewmodel.StorageCheckViewModel
import co.electriccoin.zcash.work.WorkIds import co.electriccoin.zcash.work.WorkIds
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -320,9 +321,9 @@ class MainActivity : FragmentActivity() {
} }
is SecretState.NeedsBackup -> { is SecretState.NeedsBackup -> {
WrapNewWalletRecovery( WrapSeed(
secretState.persistableWallet, args = SeedNavigationArgs.NEW_WALLET,
onBackupComplete = { walletViewModel.persistOnboardingState(OnboardingState.READY) } 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.optin.AndroidExchangeRateOptIn
import co.electriccoin.zcash.ui.screen.exchangerate.settings.AndroidSettingsExchangeRateOptIn import co.electriccoin.zcash.ui.screen.exchangerate.settings.AndroidSettingsExchangeRateOptIn
import co.electriccoin.zcash.ui.screen.exportdata.WrapExportPrivateData 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.home.WrapHome
import co.electriccoin.zcash.ui.screen.integrations.WrapIntegrations import co.electriccoin.zcash.ui.screen.integrations.WrapIntegrations
import co.electriccoin.zcash.ui.screen.paymentrequest.WrapPaymentRequest 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.request.WrapRequest
import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs
import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator 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.ext.toSerializableAddress
import co.electriccoin.zcash.ui.screen.send.model.SendArguments import co.electriccoin.zcash.ui.screen.send.model.SendArguments
import co.electriccoin.zcash.ui.screen.sendconfirmation.WrapSendConfirmation 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.SendConfirmationArguments
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SendConfirmationStage import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SendConfirmationStage
import co.electriccoin.zcash.ui.screen.settings.WrapSettings 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.update.WrapCheckForUpdate
import co.electriccoin.zcash.ui.screen.warning.WrapNotEnoughSpace import co.electriccoin.zcash.ui.screen.warning.WrapNotEnoughSpace
import co.electriccoin.zcash.ui.screen.whatsnew.WrapWhatsNew import co.electriccoin.zcash.ui.screen.whatsnew.WrapWhatsNew
@ -204,20 +205,16 @@ internal fun MainActivity.Navigation() {
WrapChooseServer() WrapChooseServer()
} }
composable(SEED_RECOVERY) { composable(SEED_RECOVERY) {
WrapSeedRecovery( WrapSeed(
goBack = { args = SeedNavigationArgs.RECOVERY,
goBackOverride = {
setSeedRecoveryAuthentication(false) setSeedRecoveryAuthentication(false)
navController.popBackStackJustOnce(SEED_RECOVERY) }
},
onDone = {
setSeedRecoveryAuthentication(false)
navController.popBackStackJustOnce(SEED_RECOVERY)
},
) )
} }
composable(SUPPORT) { composable(SUPPORT) {
// Pop back stack won't be right if we deep link into support // Pop back stack won't be right if we deep link into support
WrapSupport(goBack = { navController.popBackStackJustOnce(SUPPORT) }) WrapFeedback()
} }
composable(DELETE_WALLET) { composable(DELETE_WALLET) {
WrapDeleteWallet( WrapDeleteWallet(
@ -234,7 +231,6 @@ internal fun MainActivity.Navigation() {
composable(ABOUT) { composable(ABOUT) {
WrapAbout( WrapAbout(
goBack = { navController.popBackStackJustOnce(ABOUT) }, goBack = { navController.popBackStackJustOnce(ABOUT) },
goWhatsNew = { navController.navigateJustOnce(WHATS_NEW) }
) )
} }
composable(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 android.content.Context
import co.electriccoin.zcash.spackle.ClipboardManagerUtil import co.electriccoin.zcash.spackle.ClipboardManagerUtil
class CopyToClipboardUseCase { class CopyToClipboardUseCase(
private val context: Context
) {
operator fun invoke( operator fun invoke(
context: Context,
tag: String, tag: String,
value: String value: String
) = ClipboardManagerUtil.copyToClipboard( ) = 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 import org.koin.compose.koinInject
@Composable @Composable
internal fun WrapAbout( internal fun WrapAbout(goBack: () -> Unit) {
goBack: () -> Unit,
goWhatsNew: () -> Unit,
) {
val activity = LocalActivity.current val activity = LocalActivity.current
val walletViewModel = koinActivityViewModel<WalletViewModel>() val walletViewModel = koinActivityViewModel<WalletViewModel>()
val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value
@ -64,7 +60,6 @@ internal fun WrapAbout(
}, },
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
topAppBarSubTitleState = walletState, topAppBarSubTitleState = walletState,
onWhatsNew = goWhatsNew
) )
} }

View File

@ -1,11 +1,11 @@
package co.electriccoin.zcash.ui.screen.about.view package co.electriccoin.zcash.ui.screen.about.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -14,6 +14,7 @@ import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -22,23 +23,25 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItem
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItemState
import co.electriccoin.zcash.ui.design.component.TopAppBarBackNavigation import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar
import co.electriccoin.zcash.ui.design.component.ZashiButton 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.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.ConfigInfoFixture
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo
@ -49,12 +52,11 @@ fun About(
onBack: () -> Unit, onBack: () -> Unit,
configInfo: ConfigInfo, configInfo: ConfigInfo,
onPrivacyPolicy: () -> Unit, onPrivacyPolicy: () -> Unit,
onWhatsNew: () -> Unit,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
topAppBarSubTitleState: TopAppBarSubTitleState, topAppBarSubTitleState: TopAppBarSubTitleState,
versionInfo: VersionInfo, versionInfo: VersionInfo,
) { ) {
BlankBgScaffold( Scaffold(
topBar = { topBar = {
AboutTopAppBar( AboutTopAppBar(
onBack = onBack, onBack = onBack,
@ -68,14 +70,18 @@ fun About(
AboutMainContent( AboutMainContent(
versionInfo = versionInfo, versionInfo = versionInfo,
onPrivacyPolicy = onPrivacyPolicy, onPrivacyPolicy = onPrivacyPolicy,
onWhatsNew = onWhatsNew,
modifier = modifier =
Modifier Modifier
.fillMaxHeight() .fillMaxHeight()
.verticalScroll( .verticalScroll(
rememberScrollState() 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, configInfo: ConfigInfo,
subTitleState: TopAppBarSubTitleState subTitleState: TopAppBarSubTitleState
) { ) {
SmallTopAppBar( ZashiSmallTopAppBar(
subTitle = subtitle =
when (subTitleState) { when (subTitleState) {
TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label) TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label)
TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label) TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label)
TopAppBarSubTitleState.None -> null TopAppBarSubTitleState.None -> null
}, },
titleText = stringResource(id = R.string.about_title), title = stringResource(id = R.string.about_title),
navigationAction = { navigationAction = {
TopAppBarBackNavigation( ZashiTopAppBarBackNavigation(onBack = onBack)
backText = stringResource(id = R.string.back_navigation),
backContentDescriptionText = stringResource(R.string.back_navigation_content_description),
onBack = onBack
)
}, },
regularActions = { regularActions = {
if (versionInfo.isDebuggable && !versionInfo.isRunningUnderTestService) { if (versionInfo.isDebuggable && !versionInfo.isRunningUnderTestService) {
@ -149,84 +151,57 @@ private fun DebugMenu(
@Composable @Composable
fun AboutMainContent( fun AboutMainContent(
onWhatsNew: () -> Unit,
onPrivacyPolicy: () -> Unit, onPrivacyPolicy: () -> Unit,
versionInfo: VersionInfo, versionInfo: VersionInfo,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Column(modifier) { Column(modifier) {
Image( Text(
modifier = modifier = Modifier.padding(horizontal = ZashiDimensions.Spacing.spacingXl),
Modifier text = stringResource(id = R.string.about_subtitle),
.height(ZcashTheme.dimens.inScreenZcashTextLogoHeight) color = ZashiColors.Text.textPrimary,
.align(Alignment.CenterHorizontally), style = ZashiTypography.header6,
painter = painterResource(id = co.electriccoin.zcash.ui.design.R.drawable.zashi_text_logo_small), fontWeight = FontWeight.SemiBold
colorFilter = ColorFilter.tint(color = ZcashTheme.colors.secondaryColor),
contentDescription = stringResource(R.string.zcash_logo_content_description)
) )
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall)) Spacer(Modifier.height(12.dp))
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))
Text( Text(
modifier = Modifier.padding(horizontal = ZashiDimensions.Spacing.spacingXl),
text = stringResource(id = R.string.about_description), text = stringResource(id = R.string.about_description),
color = ZcashTheme.colors.textDescriptionDark, color = ZashiColors.Text.textPrimary,
style = ZcashTheme.extendedTypography.aboutText style = ZashiTypography.textSm
) )
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge)) Spacer(Modifier.height(32.dp))
ZashiButton( ZashiSettingsListItem(
modifier = Modifier.fillMaxWidth(), ZashiSettingsListItemState(
onClick = onWhatsNew, icon = R.drawable.ic_settings_info,
text = stringResource(R.string.about_button_whats_new), 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(), modifier = Modifier.fillMaxWidth(),
onClick = onPrivacyPolicy, version = stringRes(R.string.settings_version, versionInfo.versionName)
text = stringResource(R.string.about_button_privacy_policy),
) )
} }
} }
@PreviewScreens
@Composable @Composable
private fun AboutPreview() { private fun AboutPreview() =
About( ZcashTheme {
onBack = {}, About(
configInfo = ConfigInfoFixture.new(), onBack = {},
onPrivacyPolicy = {}, configInfo = ConfigInfoFixture.new(),
onWhatsNew = {}, onPrivacyPolicy = {},
snackbarHostState = SnackbarHostState(), snackbarHostState = SnackbarHostState(),
topAppBarSubTitleState = TopAppBarSubTitleState.None, topAppBarSubTitleState = TopAppBarSubTitleState.None,
versionInfo = VersionInfoFixture.new(), versionInfo = VersionInfoFixture.new(),
) )
}
@Preview("About")
@Composable
private fun AboutPreviewLight() =
ZcashTheme(forceDarkMode = false) {
AboutPreview()
}
@Preview("About")
@Composable
private fun AboutPreviewDark() =
ZcashTheme(forceDarkMode = true) {
AboutPreview()
} }

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.RadioButton
import co.electriccoin.zcash.ui.design.component.RadioButtonCheckedContent import co.electriccoin.zcash.ui.design.component.RadioButtonCheckedContent
import co.electriccoin.zcash.ui.design.component.RadioButtonState 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.TextFieldState
import co.electriccoin.zcash.ui.design.component.TopAppBarBackNavigation
import co.electriccoin.zcash.ui.design.component.ZashiBadge import co.electriccoin.zcash.ui.design.component.ZashiBadge
import co.electriccoin.zcash.ui.design.component.ZashiBottomBar import co.electriccoin.zcash.ui.design.component.ZashiBottomBar
import co.electriccoin.zcash.ui.design.component.ZashiButton import co.electriccoin.zcash.ui.design.component.ZashiButton
import co.electriccoin.zcash.ui.design.component.ZashiHorizontalDivider 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.ZashiTextField
import co.electriccoin.zcash.ui.design.component.ZashiTextFieldDefaults 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.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
@ -208,9 +208,9 @@ private fun ChooseServerTopAppBar(
onBack: () -> Unit, onBack: () -> Unit,
subTitleState: TopAppBarSubTitleState subTitleState: TopAppBarSubTitleState
) { ) {
SmallTopAppBar( ZashiSmallTopAppBar(
titleText = stringResource(id = R.string.choose_server_title), title = stringResource(id = R.string.choose_server_title),
subTitle = subtitle =
when (subTitleState) { when (subTitleState) {
TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label) TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label)
TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_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), modifier = Modifier.testTag(CHOOSE_SERVER_TOP_APP_BAR),
showTitleLogo = true, showTitleLogo = true,
navigationAction = { navigationAction = {
TopAppBarBackNavigation( ZashiTopAppBarBackNavigation(onBack = onBack)
backText = stringResource(id = R.string.back_navigation).uppercase(),
backContentDescriptionText = stringResource(R.string.back_navigation_content_description),
onBack = onBack
)
} }
) )
} }

View File

@ -16,26 +16,29 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource 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.R
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT 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.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.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.scaffoldPadding
import co.electriccoin.zcash.ui.design.util.stringRes
@Preview("Delete Wallet") @PreviewScreens
@Composable @Composable
private fun ExportPrivateDataPreview() { private fun ExportPrivateDataPreview() =
ZcashTheme(forceDarkMode = false) { ZcashTheme {
DeleteWallet( DeleteWallet(
snackbarHostState = SnackbarHostState(), snackbarHostState = SnackbarHostState(),
onBack = {}, onBack = {},
@ -43,7 +46,6 @@ private fun ExportPrivateDataPreview() {
topAppBarSubTitleState = TopAppBarSubTitleState.None, topAppBarSubTitleState = TopAppBarSubTitleState.None,
) )
} }
}
@Composable @Composable
fun DeleteWallet( fun DeleteWallet(
@ -77,17 +79,16 @@ private fun DeleteWalletDataTopAppBar(
onBack: () -> Unit, onBack: () -> Unit,
subTitleState: TopAppBarSubTitleState subTitleState: TopAppBarSubTitleState
) { ) {
SmallTopAppBar( ZashiSmallTopAppBar(
subTitle = title = stringResource(R.string.delete_wallet_title),
subtitle =
when (subTitleState) { when (subTitleState) {
TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label) TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label)
TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label) TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label)
TopAppBarSubTitleState.None -> null TopAppBarSubTitleState.None -> null
}, },
navigationAction = { navigationAction = {
TopAppBarBackNavigation( ZashiTopAppBarBackNavigation(
backText = stringResource(id = R.string.back_navigation).uppercase(),
backContentDescriptionText = stringResource(R.string.back_navigation_content_description),
onBack = onBack onBack = onBack
) )
} }
@ -99,46 +100,36 @@ private fun DeleteWalletContent(
onConfirm: () -> Unit, onConfirm: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val appName = stringResource(id = R.string.app_name)
Column( Column(
modifier = modifier, modifier = modifier
horizontalAlignment = Alignment.CenterHorizontally
) { ) {
TopScreenLogoTitle( Text(
title = stringResource(R.string.delete_wallet_title, appName), text = stringResource(R.string.delete_wallet_title),
logoContentDescription = stringResource(R.string.zcash_logo_content_description) style = ZashiTypography.header6,
color = ZashiColors.Text.textPrimary,
fontWeight = FontWeight.SemiBold
) )
Spacer(Modifier.height(ZcashTheme.dimens.spacingBig)) Spacer(Modifier.height(ZashiDimensions.Spacing.spacingXl))
Text( Text(
text = stringResource(R.string.delete_wallet_text_1), 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(
text = text = stringResource(R.string.delete_wallet_text_2),
stringResource( style = ZashiTypography.textSm,
R.string.delete_wallet_text_2, color = ZashiColors.Text.textPrimary,
appName
)
) )
Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault)) Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault))
val checkedState = rememberSaveable { mutableStateOf(false) } val checkedState = rememberSaveable { mutableStateOf(false) }
Row(Modifier.fillMaxWidth()) {
LabeledCheckBox(
checked = checkedState.value,
onCheckedChange = {
checkedState.value = it
},
text = stringResource(R.string.delete_wallet_acknowledge),
)
}
Spacer( Spacer(
modifier = modifier =
@ -147,13 +138,26 @@ private fun DeleteWalletContent(
.weight(MINIMAL_WEIGHT) .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( ZashiButton(
onClick = onConfirm, onClick = onConfirm,
text = stringResource(R.string.delete_wallet_button, appName), text = stringResource(R.string.delete_wallet_button),
enabled = checkedState.value, 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 package co.electriccoin.zcash.ui.screen.exchangerate.widget
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.MutableTransitionState 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.runtime.Composable
import androidx.compose.ui.Alignment 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.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup import androidx.compose.ui.window.Popup
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens import co.electriccoin.zcash.ui.common.compose.ZashiAnimatedTooltip
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
@Composable @Composable
internal fun StyledExchangeUnavailablePopup( internal fun StyledExchangeUnavailablePopup(
@ -49,88 +20,11 @@ internal fun StyledExchangeUnavailablePopup(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
offset = offset offset = offset
) { ) {
AnimatedVisibility( ZashiAnimatedTooltip(
visibleState = transitionState, visibleState = transitionState,
enter = title = stringRes(R.string.exchange_rate_unavailable_title),
fadeIn() + message = stringRes(R.string.exchange_rate_unavailable_subtitle),
slideInVertically(spring(stiffness = Spring.StiffnessHigh)) + onDismissRequest = onDismissRequest
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)
) )
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 { object ExportPrivateDataScreenTag {
const val AGREE_CHECKBOX_TAG = "agree_checkbox" const val AGREE_CHECKBOX_TAG = "agree_checkbox"
const val WARNING_TEXT_TAG = "warning_text" 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 package co.electriccoin.zcash.ui.screen.exportdata.view
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState 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.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.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.scaffoldPadding
import co.electriccoin.zcash.ui.design.util.stringRes
@Preview("Export Private Data")
@Composable
private fun ExportPrivateDataPreview() {
ZcashTheme(forceDarkMode = false) {
ExportPrivateData(
snackbarHostState = SnackbarHostState(),
onBack = {},
onAgree = {},
onConfirm = {},
topAppBarSubTitleState = TopAppBarSubTitleState.None,
)
}
}
@Composable @Composable
fun ExportPrivateData( fun ExportPrivateData(
@ -56,7 +40,7 @@ fun ExportPrivateData(
onConfirm: () -> Unit, onConfirm: () -> Unit,
topAppBarSubTitleState: TopAppBarSubTitleState, topAppBarSubTitleState: TopAppBarSubTitleState,
) { ) {
BlankBgScaffold( Scaffold(
topBar = { topBar = {
ExportPrivateDataTopAppBar( ExportPrivateDataTopAppBar(
onBack = onBack, onBack = onBack,
@ -82,19 +66,16 @@ private fun ExportPrivateDataTopAppBar(
onBack: () -> Unit, onBack: () -> Unit,
subTitleState: TopAppBarSubTitleState subTitleState: TopAppBarSubTitleState
) { ) {
SmallTopAppBar( ZashiSmallTopAppBar(
subTitle = title = stringResource(R.string.export_data_title),
subtitle =
when (subTitleState) { when (subTitleState) {
TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label) TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label)
TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label) TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label)
TopAppBarSubTitleState.None -> null TopAppBarSubTitleState.None -> null
}, },
navigationAction = { navigationAction = {
TopAppBarBackNavigation( ZashiTopAppBarBackNavigation(onBack = onBack)
backText = stringResource(id = R.string.back_navigation).uppercase(),
backContentDescriptionText = stringResource(R.string.back_navigation_content_description),
onBack = onBack
)
}, },
) )
} }
@ -105,50 +86,35 @@ private fun ExportPrivateDataContent(
onConfirm: () -> Unit, onConfirm: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column( Column(modifier = modifier) {
modifier = modifier, Text(
horizontalAlignment = Alignment.CenterHorizontally text = stringResource(R.string.export_data_header),
) { style = ZashiTypography.header6,
TopScreenLogoTitle( fontWeight = FontWeight.SemiBold,
title = stringResource(R.string.export_data_header), color = ZashiColors.Text.textPrimary
logoContentDescription = stringResource(R.string.zcash_logo_content_description)
) )
Spacer(Modifier.height(ZcashTheme.dimens.spacingLarge)) Spacer(Modifier.height(ZashiDimensions.Spacing.spacingLg))
Body(
modifier = Modifier.testTag(ExportPrivateDataScreenTag.WARNING_TEXT_TAG),
text = stringResource(R.string.export_data_text_1)
)
Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault))
Text( Text(
modifier = Modifier.testTag(ExportPrivateDataScreenTag.ADDITIONAL_TEXT_TAG), modifier = Modifier.testTag(ExportPrivateDataScreenTag.WARNING_TEXT_TAG),
text = stringResource(R.string.export_data_text_2), text = stringResource(R.string.export_data_text),
fontSize = 14.sp style = ZashiTypography.textSm,
color = ZashiColors.Text.textPrimary
) )
Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault)) Spacer(Modifier.weight(1f))
val checkedState = rememberSaveable { mutableStateOf(false) } val checkedState = rememberSaveable { mutableStateOf(false) }
Row(Modifier.fillMaxWidth()) { ZashiCheckbox(
LabeledCheckBox( modifier = Modifier.testTag(ExportPrivateDataScreenTag.AGREE_CHECKBOX_TAG),
checked = checkedState.value, isChecked = checkedState.value,
onCheckedChange = { onClick = {
checkedState.value = it val new = checkedState.value.not()
onAgree(it) checkedState.value = new
}, onAgree(new)
text = stringResource(R.string.export_data_agree), },
checkBoxTestTag = ExportPrivateDataScreenTag.AGREE_CHECKBOX_TAG text = stringRes(R.string.export_data_agree),
)
}
Spacer(
modifier =
Modifier
.fillMaxHeight()
.weight(MINIMAL_WEIGHT)
) )
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge)) 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 { ZashiBottomBar {
ZashiButton( ZashiButton(
text = stringResource(id = R.string.qr_code_share_btn), text = stringResource(id = R.string.qr_code_share_btn),
leadingIcon = painterResource(R.drawable.ic_share), icon = R.drawable.ic_share,
onClick = { state.onQrCodeShare(qrCodeImage) }, onClick = { state.onQrCodeShare(qrCodeImage) },
modifier = modifier =
Modifier Modifier
@ -202,7 +202,7 @@ private fun QrCodeBottomBar(
ZashiButton( ZashiButton(
text = stringResource(id = R.string.qr_code_copy_btn), 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) }, onClick = { state.onAddressCopy(state.walletAddress.address) },
colors = ZashiButtonDefaults.secondaryColors(), colors = ZashiButtonDefaults.secondaryColors(),
modifier = modifier =

View File

@ -87,7 +87,6 @@ class QrCodeViewModel(
private fun onAddressCopyClick(address: String) = private fun onAddressCopyClick(address: String) =
copyToClipboard( copyToClipboard(
context = application.applicationContext,
tag = application.getString(R.string.qr_code_clipboard_tag), tag = application.getString(R.string.qr_code_clipboard_tag),
value = address value = address
) )

View File

@ -33,7 +33,6 @@ class ReceiveViewModel(
isTestnet = getVersionInfo().isTestnet, isTestnet = getVersionInfo().isTestnet,
onAddressCopy = { address -> onAddressCopy = { address ->
copyToClipboard( copyToClipboard(
context = application.applicationContext,
tag = application.getString(R.string.receive_clipboard_tag), tag = application.getString(R.string.receive_clipboard_tag),
value = address value = address
) )

View File

@ -16,7 +16,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
@ -175,7 +174,7 @@ private fun RequestBottomBar(
is RequestState.QrCode -> { is RequestState.QrCode -> {
ZashiButton( ZashiButton(
text = stringResource(id = R.string.request_qr_share_btn), 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(), enabled = state.request.qrCodeState.isValid(),
onClick = { state.onQrCodeShare(state.request.qrCodeState.bitmap!!) }, onClick = { state.onQrCodeShare(state.request.qrCodeState.bitmap!!) },
modifier = modifier =

View File

@ -422,7 +422,7 @@ private fun RestoreSeedMainContent(
.then(modifier), .then(modifier),
horizontalAlignment = Alignment.CenterHorizontally 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( Column(
modifier = modifier =
Modifier.onSizeChanged { size -> Modifier.onSizeChanged { size ->
@ -478,7 +478,7 @@ private fun RestoreSeedMainContent(
} }
LaunchedEffect(parseResult) { LaunchedEffect(parseResult) {
// Causes the TextFiled to refocus // Causes the TextField to refocus
if (!isSeedValid) { if (!isSeedValid) {
focusRequester.requestFocus() focusRequester.requestFocus()
} }
@ -853,7 +853,7 @@ private fun RestoreBirthdayMainContent(
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// Causes the TextFiled to focus on the first screen visit // Causes the TextField to focus on the first screen visit
focusRequester.requestFocus() 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.ADVANCED_SETTINGS
import co.electriccoin.zcash.ui.NavigationTargets.INTEGRATIONS import co.electriccoin.zcash.ui.NavigationTargets.INTEGRATIONS
import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT 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.R
import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider
import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase
@ -140,6 +141,11 @@ class SettingsViewModel(
icon = R.drawable.ic_advanced_settings, icon = R.drawable.ic_advanced_settings,
onClick = ::onAdvancedSettingsClick onClick = ::onAdvancedSettingsClick
), ),
ZashiSettingsListItemState(
text = stringRes(R.string.settings_whats_new),
icon = R.drawable.ic_settings_whats_new,
onClick = ::onWhatsNewClick
),
ZashiSettingsListItemState( ZashiSettingsListItemState(
text = stringRes(R.string.settings_about_us), text = stringRes(R.string.settings_about_us),
icon = R.drawable.ic_settings_info, 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?> = private fun booleanStateFlow(default: BooleanPreferenceDefault): StateFlow<Boolean?> =
flow<Boolean?> { flow<Boolean?> {
emitAll(default.observe(standardPreferenceProvider())) 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 kotlinx.datetime.Instant
import java.io.File 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 // TODO [#1301]: https://github.com/Electric-Coin-Company/zashi-android/issues/1301
data class CrashInfo(val exceptionClassName: String, val isUncaught: Boolean, val timestamp: Instant) { 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 import kotlinx.datetime.LocalDate
data class WhatsNewState( data class WhatsNewState(
val version: StringResource, val titleVersion: StringResource,
val bottomVersion: StringResource,
val date: LocalDate, val date: LocalDate,
val sections: List<WhatsNewSectionState> val sections: List<WhatsNewSectionState>
) { ) {
companion object { companion object {
fun new(changelog: Changelog) = fun new(
WhatsNewState( changelog: Changelog,
version = stringRes(R.string.whats_new_version, changelog.version), version: String
date = changelog.date, ) = WhatsNewState(
sections = titleVersion = stringRes(R.string.whats_new_version, changelog.version),
listOfNotNull(changelog.added, changelog.changed, changelog.fixed, changelog.removed) bottomVersion = stringRes(R.string.settings_version, version),
.map { date = changelog.date,
WhatsNewSectionState( sections =
stringRes(value = it.title), listOfNotNull(changelog.added, changelog.changed, changelog.fixed, changelog.removed)
stringRes(it.content) .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.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.ParagraphStyle import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextIndent import androidx.compose.ui.text.style.TextIndent
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
import co.electriccoin.zcash.ui.design.component.BlankSurface import co.electriccoin.zcash.ui.design.component.BlankSurface
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar 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.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.getValue
import co.electriccoin.zcash.ui.fixture.ChangelogFixture 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.WhatsNewSectionState
import co.electriccoin.zcash.ui.screen.whatsnew.model.WhatsNewState import co.electriccoin.zcash.ui.screen.whatsnew.model.WhatsNewState
import kotlinx.datetime.toJavaLocalDate import kotlinx.datetime.toJavaLocalDate
@ -62,33 +68,42 @@ fun WhatsNewView(
) { ) {
Row { Row {
Text( Text(
text = state.version.getValue(), text = state.titleVersion.getValue(),
style = ZcashTheme.typography.primary.titleSmall, style = ZashiTypography.textXl,
fontSize = 13.sp color = ZashiColors.Text.textPrimary,
fontWeight = FontWeight.SemiBold
) )
Text( Text(
modifier = Modifier.weight(1f), modifier =
Modifier
.weight(1f)
.align(CenterVertically),
text = DateTimeFormatter.ISO_LOCAL_DATE.format(state.date.toJavaLocalDate()), text = DateTimeFormatter.ISO_LOCAL_DATE.format(state.date.toJavaLocalDate()),
textAlign = TextAlign.End, textAlign = TextAlign.End,
style = ZcashTheme.typography.primary.titleSmall, style = ZashiTypography.textSm,
fontSize = 13.sp 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 -> state.sections.forEach { section ->
WhatsNewSection(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 @Composable
private fun WhatsNewSection(state: WhatsNewSectionState) { private fun WhatsNewSection(state: WhatsNewSectionState) {
val bulletString = "\u2022\t\t" val bulletString = "\u2022 "
val bulletTextStyle = MaterialTheme.typography.bodySmall val bulletTextStyle = ZashiTypography.textSm
val bulletTextMeasurer = rememberTextMeasurer() val bulletTextMeasurer = rememberTextMeasurer()
val bulletStringWidth = val bulletStringWidth =
remember(bulletTextStyle, bulletTextMeasurer) { remember(bulletTextStyle, bulletTextMeasurer) {
@ -116,12 +131,15 @@ private fun WhatsNewSection(state: WhatsNewSectionState) {
Column { Column {
Text( Text(
text = state.title.getValue(), 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)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingMin))
Text( Text(
modifier = Modifier.padding(start = ZashiDimensions.Spacing.spacingMd),
text = bulletStyle, text = bulletStyle,
style = bulletTextStyle style = bulletTextStyle
) )
@ -142,11 +160,7 @@ private fun AppBar(
}, },
titleText = stringResource(id = R.string.whats_new_title).uppercase(), titleText = stringResource(id = R.string.whats_new_title).uppercase(),
navigationAction = { navigationAction = {
TopAppBarBackNavigation( ZashiTopAppBarBackNavigation(onBack = onBack)
backText = stringResource(id = R.string.back_navigation).uppercase(),
backContentDescriptionText = stringResource(R.string.back_navigation_content_description),
onBack = onBack
)
}, },
) )
} }
@ -155,7 +169,11 @@ private fun AppBar(
private fun WhatsNewViewPreview() { private fun WhatsNewViewPreview() {
BlankSurface { BlankSurface {
WhatsNewView( WhatsNewView(
state = WhatsNewState.new(ChangelogFixture.new()), state =
WhatsNewState.new(
changelog = ChangelogFixture.new(),
version = VersionInfoFixture.new().versionName
),
walletState = TopAppBarSubTitleState.None, walletState = TopAppBarSubTitleState.None,
onBack = {} onBack = {}
) )

View File

@ -15,7 +15,12 @@ import kotlinx.coroutines.flow.stateIn
class WhatsNewViewModel(application: Application) : AndroidViewModel(application) { class WhatsNewViewModel(application: Application) : AndroidViewModel(application) {
val state: StateFlow<WhatsNewState?> = val state: StateFlow<WhatsNewState?> =
flow { flow {
val changelog = VersionInfo.new(application).changelog val versionInfo = VersionInfo.new(application)
emit(WhatsNewState.new(changelog)) emit(
WhatsNewState.new(
changelog = versionInfo.changelog,
version = versionInfo.versionName
)
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), null) }.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"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="about_title">Acerca de</string> <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_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_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_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_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> <string name="about_button_privacy_policy">Política de Privacidad</string>
</resources> </resources>

View File

@ -1,14 +1,15 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="about_title">About</string> <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_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_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_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_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> <string name="about_button_privacy_policy">Privacy Policy</string>
</resources> </resources>

View File

@ -5,5 +5,5 @@
<string name="advanced_settings_choose_server">Elegir un servidor</string> <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_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_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> </resources>

View File

@ -5,5 +5,5 @@
<string name="advanced_settings_choose_server">Choose a Server</string> <string name="advanced_settings_choose_server">Choose a Server</string>
<string name="advanced_settings_currency_conversion">Currency Conversion</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_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> </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"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="delete_wallet_title"> <string name="delete_wallet_title">Reset Zashi</string>
Eliminar <xliff:g id="app_name" example="Zashi">%1$s</xliff:g>
</string>
<string name="delete_wallet_text_1"> <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>
<string name="delete_wallet_text_2"> <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 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.
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.
</string> </string>
<string name="delete_wallet_acknowledge">I understand</string>
<string name="delete_wallet_acknowledge">Entiendo</string> <string name="delete_wallet_button">Reset Zashi</string>
<string name="delete_wallet_failed">Wallet deletion failed. Try it again, please.</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>
</resources> </resources>

View File

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

View File

@ -1,9 +1,16 @@
<resources> <resources>
<string name="export_data_title">Data Export</string>
<string name="export_data_header">Consentimiento para Exportar Datos Privados</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">
<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> By clicking “I Agree” below, you give your consent to export Zashi\s private data which includes the entire
<string name="export_data_confirm">Exportar datos privados</string> history of the wallet, sll private information, memos, amounts, and recipient addresses, even for your
<string name="export_data_agree">Estoy de acuerdo</string> 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_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> <string name="export_data_unable_to_share">No se pudo encontrar una aplicación con la cual compartir.</string>
</resources> </resources>

View File

@ -1,14 +1,16 @@
<resources> <resources>
<string name="export_data_title">Data Export</string>
<string name="export_data_header">Consent for Exporting Private Data</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 <string name="export_data_text">
data which includes the entire history of the wallet, all private information, memos, amounts and recipient By clicking “I Agree” below, you give your consent to export Zashi\s private data which includes the entire
addresses, even for your shielded activity.*\n\nThis private data also gives the ability to see certain future history of the wallet, sll private information, memos, amounts, and recipient addresses, even for your
actions you take with Zashi.\n\nSharing this private data is irrevocable — once you have shared this private shielded activity.*\n\nThe private data also gives the ability to see certain future actions you take with
data with someone, there is no way to revoke their access.</string> Zashi.\n\nSharing this private data is irrevocable - once you have shared this private data with someone, there
<string name="export_data_text_2">*Note that this private data does not give them the ability to spend your 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> funds, only the ability to see what you do with your funds.
<string name="export_data_confirm">Export private data</string> </string>
<string name="export_data_agree">I agree</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_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> <string name="export_data_unable_to_share">Unable to find an application to share with.</string>
</resources> </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"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="support_header">Support</string> <string name="support_header">Support</string>
<string name="support_title">Send Us Feedback</string>
<string name="support_hint">How can we help?</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_send">Send</string>
<string name="support_confirmation_dialog_ok">OK</string> <string name="support_confirmation_dialog_ok">OK</string>
<string name="support_confirmation_dialog_cancel">Cancel</string> <string name="support_confirmation_dialog_cancel">Cancel</string>
<string name="support_confirmation_dialog_title">Open e-mail app</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> 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_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> </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"> <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_title">Recovery Phrase</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_header">Secure Your Wallet</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_description">The following 24 words are the keys to your funds and are the only way
<string name="seed_recovery_button_finished">¡Entendido!</string> to recover your funds if you get locked out or get a new device.</string>
<string name="seed_recovery_copy">Toca para copiar</string> <string name="seed_recovery_reveal_button">Reveal security details</string>
<string name="seed_recovery_seed_clipboard_tag">Frase de Semilla de Zcash</string> <string name="seed_recovery_hide_button">Hide security details</string>
<string name="seed_recovery_birthday_clipboard_tag">Cumpleaños de la Billetera Zcash</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> </resources>

View File

@ -1,12 +1,18 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <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_title">Recovery Phrase</string>
<string name="seed_recovery_description">The following 24 words are the keys to your funds and are the only way to <string name="seed_recovery_header">Secure Your Wallet</string>
recover your funds if you get locked out or get a new device. Protect your ZEC by storing this phrase in a <string name="seed_recovery_description">The following 24 words are the keys to your funds and are the only way
place you trust and never share it with anyone!</string> to recover your funds if you get locked out or get a new device.</string>
<string name="seed_recovery_birthday_height" formatted="true">Wallet birthday height: <xliff:g example="419200" <string name="seed_recovery_reveal_button">Reveal security details</string>
id="birthday_height">%1$d</xliff:g></string> <string name="seed_recovery_hide_button">Hide security details</string>
<string name="seed_recovery_button_finished">I got it!</string> <string name="seed_recovery_next_button">Next</string>
<string name="seed_recovery_copy">Tap to Copy</string> <string name="seed_recovery_phrase_title">Recovery Phrase</string>
<string name="seed_recovery_seed_clipboard_tag">Zcash Seed Phrase</string> <string name="seed_recovery_bday_title">Wallet Birthday Height</string>
<string name="seed_recovery_birthday_clipboard_tag">Zcash Wallet Birthday</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> </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