* [#946] Warning screen UI [#946] Code review changes moved security warning to separate folder changed main heading text style changed static version text to dynamic Co-authored-by: Honza Rychnovský <rychnovsky.honza@gmail.com> * [#993] New Primary Button Click Animation * [#946] Warning Screen [UI][Logic][Tests] - Security Warning screen incorporated into the app onboarding logic - NeedsWarning wallet state persisted and is required if the user re-runs next time, and the Security Warning screen still needs to be presented - Privacy Policy opened by part of the screen text clicking in an external web browser app - Unit tests - UI tests - Screenshot tests --------- Co-authored-by: Venkat-corebts <143575548+Venkat-corebts@users.noreply.github.com>
This commit is contained in:
parent
c7e5394940
commit
57a133a12c
|
@ -1,7 +1,11 @@
|
|||
package co.electriccoin.zcash.ui.design.component
|
||||
|
||||
import android.graphics.BlurMaskFilter
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.gestures.waitForUpOrCancellation
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
|
@ -13,14 +17,21 @@ import androidx.compose.material3.ButtonDefaults.buttonColors
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
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.composed
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.PaintingStyle
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
@ -71,6 +82,10 @@ fun PrimaryButton(
|
|||
blurRadius = 0.dp,
|
||||
stroke = textColor != MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
.translationClick(
|
||||
translationX = ZcashTheme.dimens.shadowOffsetX + 6.dp, // + 6dp to exactly cover the bottom shadow
|
||||
translationY = ZcashTheme.dimens.shadowOffsetX + 6.dp
|
||||
)
|
||||
.defaultMinSize(ZcashTheme.dimens.defaultButtonWidth, ZcashTheme.dimens.defaultButtonHeight)
|
||||
.border(1.dp, Color.Black),
|
||||
colors = buttonColors(
|
||||
|
@ -253,3 +268,51 @@ fun Modifier.shadow(
|
|||
}
|
||||
}
|
||||
)
|
||||
|
||||
private enum class ButtonState { Pressed, Idle }
|
||||
fun Modifier.translationClick(
|
||||
translationX: Dp = 0.dp,
|
||||
translationY: Dp = 0.dp
|
||||
) = composed {
|
||||
var buttonState by remember { mutableStateOf(ButtonState.Idle) }
|
||||
|
||||
val translationXAnimated by animateFloatAsState(
|
||||
targetValue = if (buttonState == ButtonState.Pressed) {
|
||||
translationX.value
|
||||
} else {
|
||||
0f
|
||||
},
|
||||
label = "ClickTranslationXAnimation",
|
||||
animationSpec = tween(
|
||||
durationMillis = 100
|
||||
)
|
||||
)
|
||||
val translationYAnimated by animateFloatAsState(
|
||||
targetValue = if (buttonState == ButtonState.Pressed) {
|
||||
translationY.value
|
||||
} else {
|
||||
0f
|
||||
},
|
||||
label = "ClickTranslationYAnimation",
|
||||
animationSpec = tween(
|
||||
durationMillis = 100
|
||||
)
|
||||
)
|
||||
|
||||
this
|
||||
.graphicsLayer {
|
||||
this.translationX = translationXAnimated
|
||||
this.translationY = translationYAnimated
|
||||
}
|
||||
.pointerInput(buttonState) {
|
||||
awaitPointerEventScope {
|
||||
buttonState = if (buttonState == ButtonState.Pressed) {
|
||||
waitForUpOrCancellation()
|
||||
ButtonState.Idle
|
||||
} else {
|
||||
awaitFirstDown(false)
|
||||
ButtonState.Pressed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,15 +2,17 @@ package co.electriccoin.zcash.ui.design.component
|
|||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.platform.testTag
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
|
||||
@Preview
|
||||
|
@ -18,24 +20,57 @@ import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
|||
private fun ComposablePreview() {
|
||||
val checkBoxState = remember { mutableStateOf(false) }
|
||||
ZcashTheme(darkTheme = false) {
|
||||
CheckBox(text = "test", onCheckedChange = { checkBoxState.value = it }, checked = checkBoxState.value)
|
||||
CheckBox(
|
||||
onCheckedChange = { checkBoxState.value = it },
|
||||
text = "test",
|
||||
checked = checkBoxState.value,
|
||||
checkBoxTestTag = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CheckBox(
|
||||
onCheckedChange: ((Boolean) -> Unit),
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
checked: Boolean = false
|
||||
checked: Boolean = false,
|
||||
checkBoxTestTag: String? = null
|
||||
) {
|
||||
Row(modifier = Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
enabled = true,
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
) {
|
||||
val checkBoxModifier = Modifier
|
||||
.padding(
|
||||
top = ZcashTheme.dimens.spacingTiny,
|
||||
bottom = ZcashTheme.dimens.spacingTiny,
|
||||
end = ZcashTheme.dimens.spacingTiny
|
||||
)
|
||||
.then(
|
||||
if (checkBoxTestTag != null) {
|
||||
Modifier.testTag(checkBoxTestTag)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
val (checkedState, setCheckedState) = rememberSaveable { mutableStateOf(checked) }
|
||||
Checkbox(
|
||||
checked = checkedState,
|
||||
onCheckedChange = {
|
||||
setCheckedState(it)
|
||||
onCheckedChange(it)
|
||||
},
|
||||
enabled = true,
|
||||
modifier = checkBoxModifier
|
||||
)
|
||||
ClickableText(
|
||||
onClick = {
|
||||
setCheckedState(!checkedState)
|
||||
onCheckedChange(!checkedState)
|
||||
},
|
||||
text = AnnotatedString(text),
|
||||
style = ZcashTheme.extendedTypography.checkboxText
|
||||
)
|
||||
Text(text = text)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.googlefonts.Font
|
||||
import androidx.compose.ui.text.googlefonts.GoogleFont
|
||||
import androidx.compose.ui.text.style.BaselineShift
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.sp
|
||||
import co.electriccoin.zcash.ui.design.R
|
||||
|
||||
|
@ -91,6 +92,12 @@ internal val SecondaryTypography = Typography(
|
|||
fontFamily = ArchivoFontFamily,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp
|
||||
),
|
||||
headlineMedium = TextStyle(
|
||||
fontFamily = ArchivoFontFamily,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 25.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -106,6 +113,8 @@ data class ExtendedTypography(
|
|||
val listItem: TextStyle,
|
||||
val zecBalance: TextStyle,
|
||||
val buttonText: TextStyle,
|
||||
val checkboxText: TextStyle,
|
||||
val securityWarningText: TextStyle
|
||||
)
|
||||
|
||||
@Suppress("CompositionLocalAllowlist")
|
||||
|
@ -135,5 +144,11 @@ val LocalExtendedTypography = staticCompositionLocalOf {
|
|||
buttonText = PrimaryTypography.bodySmall.copy(
|
||||
fontSize = 14.sp
|
||||
),
|
||||
checkboxText = PrimaryTypography.bodyMedium.copy(
|
||||
fontSize = 14.sp
|
||||
),
|
||||
securityWarningText = PrimaryTypography.bodySmall.copy(
|
||||
lineHeight = 22.32.sp
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ android {
|
|||
"src/main/res/ui/update",
|
||||
"src/main/res/ui/wallet_address",
|
||||
"src/main/res/ui/warning",
|
||||
"src/main/res/ui/security_warning"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package co.electriccoin.zcash.ui.screen.securitywarning.util
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.test.filters.SmallTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertContains
|
||||
|
||||
class WebBrowserUtilTest {
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun check_intent_for_web_browser() {
|
||||
val intent = WebBrowserUtil.newActivityIntent(WebBrowserUtil.ZCASH_PRIVACY_POLICY_URI)
|
||||
assertEquals(intent.action, Intent.ACTION_VIEW)
|
||||
assertEquals(WebBrowserUtil.FLAGS, intent.flags)
|
||||
assertContains(WebBrowserUtil.ZCASH_PRIVACY_POLICY_URI, intent.data.toString())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
package co.electriccoin.zcash.ui.screen.securitywarning.view
|
||||
|
||||
import androidx.compose.ui.test.assertHasClickAction
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
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.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
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 org.junit.Rule
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class SecurityWarningViewTest : UiTestPrerequisites() {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun default_ui_state_test() {
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
assertEquals(0, testSetup.getOnBackCount())
|
||||
assertEquals(false, testSetup.getOnAcknowledged())
|
||||
assertEquals(0, testSetup.getOnConfirmCount())
|
||||
|
||||
composeTestRule.onNodeWithTag(SecurityScreenTag.ACKNOWLEDGE_CHECKBOX_TAG).also {
|
||||
it.assertExists()
|
||||
it.assertIsDisplayed()
|
||||
it.assertHasClickAction()
|
||||
it.assertIsEnabled()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.security_warning_confirm), ignoreCase = true).also {
|
||||
it.assertExists()
|
||||
it.assertIsDisplayed()
|
||||
it.assertHasClickAction()
|
||||
it.assertIsNotEnabled()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(SecurityScreenTag.WARNING_TEXT_TAG).also {
|
||||
it.assertExists()
|
||||
it.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun back_test() {
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
assertEquals(0, testSetup.getOnBackCount())
|
||||
|
||||
composeTestRule.clickBack()
|
||||
|
||||
assertEquals(1, testSetup.getOnBackCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun click_disabled_confirm_button_test() {
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
assertEquals(0, testSetup.getOnConfirmCount())
|
||||
assertEquals(false, testSetup.getOnAcknowledged())
|
||||
|
||||
composeTestRule.clickConfirm()
|
||||
|
||||
assertEquals(0, testSetup.getOnConfirmCount())
|
||||
assertEquals(false, testSetup.getOnAcknowledged())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun click_enabled_confirm_button_test() {
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
assertEquals(0, testSetup.getOnConfirmCount())
|
||||
assertEquals(false, testSetup.getOnAcknowledged())
|
||||
|
||||
composeTestRule.clickAcknowledge()
|
||||
|
||||
assertEquals(0, testSetup.getOnConfirmCount())
|
||||
assertEquals(true, testSetup.getOnAcknowledged())
|
||||
|
||||
composeTestRule.clickConfirm()
|
||||
|
||||
assertEquals(1, testSetup.getOnConfirmCount())
|
||||
assertEquals(true, testSetup.getOnAcknowledged())
|
||||
}
|
||||
|
||||
private fun newTestSetup() = SecurityWarningViewTestSetup(composeTestRule).apply {
|
||||
setDefaultContent()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.clickBack() {
|
||||
onNodeWithContentDescription(getStringResource(R.string.support_back_content_description)).also {
|
||||
it.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.clickConfirm() {
|
||||
onNodeWithText(getStringResource(R.string.security_warning_confirm), ignoreCase = true).also {
|
||||
it.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.clickAcknowledge() {
|
||||
onNodeWithText(getStringResource(R.string.security_warning_acknowledge)).also {
|
||||
it.performClick()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package co.electriccoin.zcash.ui.screen.securitywarning.view
|
||||
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import co.electriccoin.zcash.spackle.getPackageInfoCompat
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.screen.about.model.VersionInfo
|
||||
import co.electriccoin.zcash.ui.test.getAppContext
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class SecurityWarningViewTestSetup(private val composeTestRule: ComposeContentTestRule) {
|
||||
|
||||
private val onBackCount = AtomicInteger(0)
|
||||
|
||||
private val onAcknowledged = AtomicBoolean(false)
|
||||
|
||||
private val onConfirmCount = AtomicInteger(0)
|
||||
|
||||
fun getOnBackCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onBackCount.get()
|
||||
}
|
||||
|
||||
fun getOnAcknowledged(): Boolean {
|
||||
composeTestRule.waitForIdle()
|
||||
return onAcknowledged.get()
|
||||
}
|
||||
|
||||
fun getOnConfirmCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onConfirmCount.get()
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("TestFunctionName")
|
||||
fun DefaultContent() {
|
||||
SecurityWarning(
|
||||
SnackbarHostState(),
|
||||
onBack = {
|
||||
onBackCount.incrementAndGet()
|
||||
},
|
||||
onPrivacyPolicy = {
|
||||
// Not tested yet. UI testing of clicking on an AnnotatedString Text part is complicated.
|
||||
},
|
||||
onAcknowledged = {
|
||||
onAcknowledged.getAndSet(it)
|
||||
},
|
||||
onConfirm = {
|
||||
onConfirmCount.incrementAndGet()
|
||||
},
|
||||
versionInfo = VersionInfo.new(
|
||||
getAppContext().packageManager.getPackageInfoCompat(getAppContext().packageName, 0L)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun setDefaultContent() {
|
||||
composeTestRule.setContent {
|
||||
ZcashTheme {
|
||||
DefaultContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,6 +18,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.NavHostController
|
||||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
||||
import cash.z.ecc.android.sdk.model.SeedPhrase
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.sdk.type.fromResources
|
||||
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
|
||||
import co.electriccoin.zcash.ui.common.BindCompLocalProvider
|
||||
import co.electriccoin.zcash.ui.configuration.RemoteConfig
|
||||
import co.electriccoin.zcash.ui.design.component.ConfigurationOverride
|
||||
|
@ -25,10 +30,13 @@ import co.electriccoin.zcash.ui.design.component.GradientSurface
|
|||
import co.electriccoin.zcash.ui.design.component.Override
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.screen.backup.WrapNewWallet
|
||||
import co.electriccoin.zcash.ui.screen.home.model.OnboardingState
|
||||
import co.electriccoin.zcash.ui.screen.home.viewmodel.HomeViewModel
|
||||
import co.electriccoin.zcash.ui.screen.home.viewmodel.SecretState
|
||||
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.WrapOnboarding
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.persistExistingWalletWithSeedPhrase
|
||||
import co.electriccoin.zcash.ui.screen.securitywarning.WrapSecurityWarning
|
||||
import co.electriccoin.zcash.ui.screen.warning.WrapNotEnoughSpace
|
||||
import co.electriccoin.zcash.ui.screen.warning.viewmodel.StorageCheckViewModel
|
||||
import co.electriccoin.zcash.work.WorkIds
|
||||
|
@ -128,10 +136,29 @@ class MainActivity : ComponentActivity() {
|
|||
SecretState.None -> {
|
||||
WrapOnboarding()
|
||||
}
|
||||
is SecretState.NeedsWarning -> {
|
||||
WrapSecurityWarning(
|
||||
onBack = { walletViewModel.persistOnboardingState(OnboardingState.NONE) },
|
||||
onConfirm = {
|
||||
walletViewModel.persistOnboardingState(OnboardingState.NEEDS_BACKUP)
|
||||
|
||||
if (FirebaseTestLabUtil.isFirebaseTestLab(applicationContext)) {
|
||||
persistExistingWalletWithSeedPhrase(
|
||||
applicationContext,
|
||||
walletViewModel,
|
||||
SeedPhrase.new(WalletFixture.Alice.seedPhrase),
|
||||
WalletFixture.Alice.getBirthday(ZcashNetwork.fromResources(applicationContext))
|
||||
)
|
||||
} else {
|
||||
walletViewModel.persistNewWallet()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
is SecretState.NeedsBackup -> {
|
||||
WrapNewWallet(
|
||||
secretState.persistableWallet,
|
||||
onBackupComplete = { walletViewModel.persistBackupComplete() }
|
||||
onBackupComplete = { walletViewModel.persistOnboardingState(OnboardingState.READY) }
|
||||
)
|
||||
}
|
||||
is SecretState.Ready -> {
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
package co.electriccoin.zcash.ui.preference
|
||||
|
||||
import co.electriccoin.zcash.preference.model.entry.BooleanPreferenceDefault
|
||||
import co.electriccoin.zcash.preference.model.entry.IntegerPreferenceDefault
|
||||
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
|
||||
import co.electriccoin.zcash.ui.screen.home.model.OnboardingState
|
||||
|
||||
object StandardPreferenceKeys {
|
||||
|
||||
/**
|
||||
* Whether the user has completed the backup flow for a newly created wallet.
|
||||
* State defining whether the user has completed any of the onboarding wallet states.
|
||||
*/
|
||||
val IS_USER_BACKUP_COMPLETE = BooleanPreferenceDefault(PreferenceKey("is_user_backup_complete"), false)
|
||||
val ONBOARDING_STATE = IntegerPreferenceDefault(
|
||||
PreferenceKey("onboarding_state"),
|
||||
OnboardingState.NONE.toNumber()
|
||||
)
|
||||
|
||||
// Default to true until https://github.com/zcash/secant-android-wallet/issues/304
|
||||
val IS_ANALYTICS_ENABLED = BooleanPreferenceDefault(PreferenceKey("is_analytics_enabled"), true)
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package co.electriccoin.zcash.ui.screen.home.model
|
||||
|
||||
/**
|
||||
* Common Onboarding/SecurityWarning/Backup screen enum.
|
||||
*
|
||||
* WARN: Do NOT re-order or change the values, it would update their ordinal numbers, and thus break screens ordering.
|
||||
*/
|
||||
enum class OnboardingState {
|
||||
NONE,
|
||||
NEEDS_WARN,
|
||||
NEEDS_BACKUP,
|
||||
READY;
|
||||
|
||||
fun toNumber() = ordinal
|
||||
|
||||
companion object {
|
||||
fun fromNumber(ordinal: Int) = entries[ordinal]
|
||||
}
|
||||
}
|
|
@ -32,6 +32,7 @@ import co.electriccoin.zcash.ui.preference.EncryptedPreferenceSingleton
|
|||
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
|
||||
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
|
||||
import co.electriccoin.zcash.ui.screen.history.state.TransactionHistorySyncState
|
||||
import co.electriccoin.zcash.ui.screen.home.model.OnboardingState
|
||||
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -93,22 +94,32 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||
)
|
||||
|
||||
/**
|
||||
* A flow of whether a backup of the user's wallet has been performed.
|
||||
* A flow of the wallet onboarding state.
|
||||
*/
|
||||
private val isBackupComplete = flow {
|
||||
private val onboardingState = flow {
|
||||
val preferenceProvider = StandardPreferenceSingleton.getInstance(application)
|
||||
emitAll(StandardPreferenceKeys.IS_USER_BACKUP_COMPLETE.observe(preferenceProvider))
|
||||
emitAll(
|
||||
StandardPreferenceKeys.ONBOARDING_STATE.observe(preferenceProvider).map { persistedNumber ->
|
||||
OnboardingState.fromNumber(persistedNumber)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val secretState: StateFlow<SecretState> = walletCoordinator.persistableWallet
|
||||
.combine(isBackupComplete) { persistableWallet: PersistableWallet?, isBackupComplete: Boolean ->
|
||||
if (null == persistableWallet) {
|
||||
SecretState.None
|
||||
} else if (!isBackupComplete) {
|
||||
val secretState: StateFlow<SecretState> = combine(
|
||||
walletCoordinator.persistableWallet,
|
||||
onboardingState
|
||||
) { persistableWallet: PersistableWallet?, onboardingState: OnboardingState ->
|
||||
when {
|
||||
onboardingState == OnboardingState.NONE -> SecretState.None
|
||||
onboardingState == OnboardingState.NEEDS_WARN -> SecretState.NeedsWarning
|
||||
onboardingState == OnboardingState.NEEDS_BACKUP && persistableWallet != null -> {
|
||||
SecretState.NeedsBackup(persistableWallet)
|
||||
} else {
|
||||
}
|
||||
onboardingState == OnboardingState.READY && persistableWallet != null -> {
|
||||
SecretState.Ready(persistableWallet)
|
||||
}
|
||||
else -> SecretState.None
|
||||
}
|
||||
}.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
|
@ -229,18 +240,18 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||
* is ready to use. Clients observe [secretState] to see the side effects. This would be used
|
||||
* for a user creating a new wallet.
|
||||
*/
|
||||
fun persistBackupComplete() {
|
||||
fun persistOnboardingState(onboardingState: OnboardingState) {
|
||||
val application = getApplication<Application>()
|
||||
|
||||
viewModelScope.launch {
|
||||
val preferenceProvider = StandardPreferenceSingleton.getInstance(application)
|
||||
|
||||
// Use the Mutex here to avoid timing issues. During wallet restore, persistBackupComplete()
|
||||
// is called prior to persistExistingWallet(). Although persistBackupComplete() should
|
||||
// Use the Mutex here to avoid timing issues. During wallet restore, persistOnboardingState()
|
||||
// is called prior to persistExistingWallet(). Although persistOnboardingState() should
|
||||
// complete quickly, it isn't guaranteed to complete before persistExistingWallet()
|
||||
// unless a mutex is used here.
|
||||
persistWalletMutex.withLock {
|
||||
StandardPreferenceKeys.IS_USER_BACKUP_COMPLETE.putValue(preferenceProvider, true)
|
||||
StandardPreferenceKeys.ONBOARDING_STATE.putValue(preferenceProvider, onboardingState.toNumber())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -270,6 +281,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||
sealed class SecretState {
|
||||
object Loading : SecretState()
|
||||
object None : SecretState()
|
||||
object NeedsWarning : SecretState()
|
||||
class NeedsBackup(val persistableWallet: PersistableWallet) : SecretState()
|
||||
class Ready(val persistableWallet: PersistableWallet) : SecretState()
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import co.electriccoin.zcash.ui.BuildConfig
|
|||
import co.electriccoin.zcash.ui.MainActivity
|
||||
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
|
||||
import co.electriccoin.zcash.ui.configuration.RemoteConfig
|
||||
import co.electriccoin.zcash.ui.screen.home.model.OnboardingState
|
||||
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.view.LongOnboarding
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.view.ShortOnboarding
|
||||
|
@ -51,20 +52,11 @@ internal fun WrapOnboarding(
|
|||
!EmulatorWtfUtil.isEmulatorWtf(applicationContext)
|
||||
|
||||
// TODO [#383]: https://github.com/zcash/secant-android-wallet/issues/383
|
||||
// TODO [#383]: Refactoring of UI state retention into rememberSaveable fields
|
||||
if (!onboardingViewModel.isImporting.collectAsStateWithLifecycle().value) {
|
||||
val onCreateWallet = {
|
||||
if (FirebaseTestLabUtil.isFirebaseTestLab(applicationContext)) {
|
||||
persistExistingWalletWithSeedPhrase(
|
||||
applicationContext,
|
||||
walletViewModel,
|
||||
SeedPhrase.new(WalletFixture.Alice.seedPhrase),
|
||||
birthday = WalletFixture.Alice.getBirthday(ZcashNetwork.fromResources(applicationContext))
|
||||
)
|
||||
} else {
|
||||
walletViewModel.persistNewWallet()
|
||||
walletViewModel.persistOnboardingState(OnboardingState.NEEDS_WARN)
|
||||
}
|
||||
}
|
||||
|
||||
val onImportWallet = {
|
||||
// In the case of the app currently being messed with by the robo test runner on
|
||||
// Firebase Test Lab or Google Play pre-launch report, we want to skip creating
|
||||
|
@ -130,7 +122,7 @@ internal fun persistExistingWalletWithSeedPhrase(
|
|||
seedPhrase: SeedPhrase,
|
||||
birthday: BlockHeight?
|
||||
) {
|
||||
walletViewModel.persistBackupComplete()
|
||||
walletViewModel.persistOnboardingState(OnboardingState.READY)
|
||||
|
||||
val network = ZcashNetwork.fromResources(context)
|
||||
val restoredWallet = PersistableWallet(
|
||||
|
|
|
@ -71,7 +71,6 @@ private fun ComposablePreview() {
|
|||
* @param onCreateWallet Callback when the user decides to create a new wallet.
|
||||
*/
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Suppress("LongParameterList")
|
||||
fun LongOnboarding(
|
||||
onboardingState: OnboardingState,
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
package co.electriccoin.zcash.ui.screen.securitywarning
|
||||
|
||||
import android.content.Context
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import co.electriccoin.zcash.configuration.AndroidConfigurationFactory
|
||||
import co.electriccoin.zcash.spackle.getPackageInfoCompat
|
||||
import co.electriccoin.zcash.ui.MainActivity
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.screen.about.model.VersionInfo
|
||||
import co.electriccoin.zcash.ui.screen.securitywarning.util.WebBrowserUtil
|
||||
import co.electriccoin.zcash.ui.screen.securitywarning.view.SecurityWarning
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
internal fun MainActivity.WrapSecurityWarning(
|
||||
onBack: () -> Unit,
|
||||
onConfirm: () -> Unit
|
||||
) {
|
||||
WrapSecurityWarning(
|
||||
this,
|
||||
onBack = onBack,
|
||||
onConfirm = onConfirm
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun WrapSecurityWarning(
|
||||
activity: ComponentActivity,
|
||||
onBack: () -> Unit,
|
||||
onConfirm: () -> Unit
|
||||
) {
|
||||
val packageInfo = activity.packageManager.getPackageInfoCompat(activity.packageName, 0L)
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
SecurityWarning(
|
||||
snackbarHostState = snackbarHostState,
|
||||
versionInfo = VersionInfo.new(packageInfo),
|
||||
onBack = onBack,
|
||||
onAcknowledged = {
|
||||
// Needed for UI testing only
|
||||
},
|
||||
onPrivacyPolicy = {
|
||||
openPrivacyPolicyInWebBrowser(
|
||||
activity.applicationContext,
|
||||
snackbarHostState,
|
||||
scope
|
||||
)
|
||||
},
|
||||
onConfirm = onConfirm
|
||||
)
|
||||
|
||||
LaunchedEffect(key1 = true) {
|
||||
AndroidConfigurationFactory.getInstance(activity.applicationContext).hintToRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun openPrivacyPolicyInWebBrowser(
|
||||
context: Context,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
val storeIntent = WebBrowserUtil.newActivityIntent(WebBrowserUtil.ZCASH_PRIVACY_POLICY_URI)
|
||||
runCatching {
|
||||
context.startActivity(storeIntent)
|
||||
}.onFailure {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = context.getString(R.string.security_warning_unable_to_web_browser)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package co.electriccoin.zcash.ui.screen.securitywarning.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
|
||||
object WebBrowserUtil {
|
||||
|
||||
const val FLAGS = Intent.FLAG_ACTIVITY_NO_HISTORY or
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_MULTIPLE_TASK
|
||||
|
||||
const val ZCASH_PRIVACY_POLICY_URI = "https://z.cash/privacy-policy/"
|
||||
|
||||
/**
|
||||
* Returns new action view app intent. We assume the a web browser app is installed.
|
||||
*
|
||||
* @param url The webpage url to open
|
||||
*
|
||||
* @return Intent for launching in a browser app.
|
||||
*/
|
||||
internal fun newActivityIntent(url: String): Intent {
|
||||
val storeUri = Uri.parse(url)
|
||||
val storeIntent = Intent(Intent.ACTION_VIEW, storeUri)
|
||||
|
||||
// To properly handle the browser backstack while navigate back to our app
|
||||
storeIntent.addFlags(FLAGS)
|
||||
|
||||
return storeIntent
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package co.electriccoin.zcash.ui.screen.securitywarning.view
|
||||
|
||||
/**
|
||||
* These are only used for automated testing.
|
||||
*/
|
||||
object SecurityScreenTag {
|
||||
const val ACKNOWLEDGE_CHECKBOX_TAG = "acknowledge_checkbox"
|
||||
const val WARNING_TEXT_TAG = "warning_text"
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
package co.electriccoin.zcash.ui.screen.securitywarning.view
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.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.text.ClickableText
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
||||
import co.electriccoin.zcash.ui.design.component.CheckBox
|
||||
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
||||
import co.electriccoin.zcash.ui.design.component.PrimaryButton
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
|
||||
import co.electriccoin.zcash.ui.screen.about.model.VersionInfo
|
||||
|
||||
@Preview("Security Warning")
|
||||
@Composable
|
||||
private fun SecurityWarningPreview() {
|
||||
ZcashTheme {
|
||||
GradientSurface {
|
||||
SecurityWarning(
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
versionInfo = VersionInfoFixture.new(),
|
||||
onBack = {},
|
||||
onAcknowledged = {},
|
||||
onPrivacyPolicy = {},
|
||||
onConfirm = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
fun SecurityWarning(
|
||||
snackbarHostState: SnackbarHostState,
|
||||
versionInfo: VersionInfo,
|
||||
onBack: () -> Unit,
|
||||
onAcknowledged: (Boolean) -> Unit,
|
||||
onPrivacyPolicy: () -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { SecurityWarningTopAppBar(onBack = onBack) },
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
) { paddingValues ->
|
||||
SecurityWarningContent(
|
||||
versionInfo = versionInfo,
|
||||
onPrivacyPolicy = onPrivacyPolicy,
|
||||
onAcknowledged = onAcknowledged,
|
||||
onConfirm = onConfirm,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = paddingValues.calculateBottomPadding(),
|
||||
start = ZcashTheme.dimens.spacingHuge,
|
||||
end = ZcashTheme.dimens.spacingHuge
|
||||
)
|
||||
.verticalScroll(rememberScrollState())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
private fun SecurityWarningTopAppBar(
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.security_warning_back).uppercase(),
|
||||
style = ZcashTheme.typography.primary.bodyMedium
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.security_warning_back_content_description)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SecurityWarningContent(
|
||||
versionInfo: VersionInfo,
|
||||
onPrivacyPolicy: () -> Unit,
|
||||
onAcknowledged: (Boolean) -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Image(
|
||||
painterResource(id = R.drawable.zashi_logo_without_text),
|
||||
stringResource(R.string.zcash_logo_content_description),
|
||||
Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(ZcashTheme.dimens.spacingXlarge))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.security_warning_header),
|
||||
style = ZcashTheme.typography.secondary.headlineMedium,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
|
||||
SecurityWarningContentText(
|
||||
versionInfo = versionInfo,
|
||||
onPrivacyPolicy = onPrivacyPolicy
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault))
|
||||
|
||||
val checkedState = rememberSaveable { mutableStateOf(false) }
|
||||
CheckBox(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Start)
|
||||
.fillMaxWidth(),
|
||||
checked = checkedState.value,
|
||||
onCheckedChange = {
|
||||
checkedState.value = it
|
||||
onAcknowledged(it)
|
||||
},
|
||||
text = stringResource(R.string.security_warning_acknowledge),
|
||||
checkBoxTestTag = SecurityScreenTag.ACKNOWLEDGE_CHECKBOX_TAG
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.weight(MINIMAL_WEIGHT)
|
||||
)
|
||||
|
||||
PrimaryButton(
|
||||
onClick = onConfirm,
|
||||
text = stringResource(R.string.security_warning_confirm).uppercase(),
|
||||
enabled = checkedState.value
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(ZcashTheme.dimens.spacingXlarge))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SecurityWarningContentText(
|
||||
versionInfo: VersionInfo,
|
||||
onPrivacyPolicy: () -> Unit,
|
||||
) {
|
||||
val textPart1 = stringResource(R.string.security_warning_text_part_1, versionInfo.versionName)
|
||||
val textPart2 = stringResource(R.string.security_warning_text_part_2)
|
||||
ClickableText(
|
||||
text = buildAnnotatedString {
|
||||
append(textPart1)
|
||||
withStyle(SpanStyle(textDecoration = TextDecoration.Underline)) {
|
||||
append(textPart2)
|
||||
}
|
||||
append(stringResource(R.string.security_warning_text_part_3))
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(stringResource(R.string.security_warning_text_part_4))
|
||||
}
|
||||
append(stringResource(R.string.security_warning_text_part_5))
|
||||
},
|
||||
style = ZcashTheme.extendedTypography.securityWarningText,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(SecurityScreenTag.WARNING_TEXT_TAG),
|
||||
onClick = { letterOffset ->
|
||||
// Call the callback only if user clicked the underlined part
|
||||
if (letterOffset >= textPart1.length && letterOffset <= (textPart1.length + textPart2.length)) {
|
||||
onPrivacyPolicy()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="36dp"
|
||||
android:height="47dp"
|
||||
android:viewportWidth="36"
|
||||
android:viewportHeight="47">
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M19.331,33.909L24.858,45L10.45,38.564L35,25.787L33.016,18.781L2,2L22.511,27.926L33.016,18.781L10.45,38.564"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#0A141F"/>
|
||||
</vector>
|
|
@ -0,0 +1,19 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="security_warning_text_part_1" formatted="true">Zashi <xliff:g example="0.2.0" id="version_name">%1$s
|
||||
</xliff:g>is a Zcash-only shielded wallet, built by Zcashers for Zcashers. The purpose of this release is
|
||||
primarily to test functionality and collect feedback. While Zashi has been engineered for your privacy and
|
||||
safety (read the privacy policy\u0020</string>
|
||||
<string name="security_warning_text_part_2">here</string>
|
||||
<string name="security_warning_text_part_3">), this release has not yet been security audited.</string>
|
||||
<string name="security_warning_text_part_4">\u0020Users are cautioned to deposit, send, and receive only small
|
||||
amounts
|
||||
of ZEC.</string>
|
||||
<string name="security_warning_text_part_5">\u0020Please click below to proceed.</string>
|
||||
<string name="security_warning_confirm">confirm</string>
|
||||
<string name="security_warning_acknowledge">I acknowledge</string>
|
||||
<string name="security_warning_back_content_description">Back</string>
|
||||
<string name="zcash_logo_content_description">Zcash logo</string>
|
||||
<string name="security_warning_header">Security warning:</string>
|
||||
<string name="security_warning_back">back</string>
|
||||
<string name="security_warning_unable_to_web_browser">Unable to find a web browser app.</string>
|
||||
</resources>
|
|
@ -329,6 +329,7 @@ private fun onboardingScreenshots(
|
|||
composeTestRule.activity.walletViewModel.secretState.value is SecretState.None
|
||||
}
|
||||
if (ConfigurationEntries.IS_SHORT_ONBOARDING_UX.getValue(emptyConfiguration)) {
|
||||
// Welcome screen
|
||||
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_short_header)).also {
|
||||
it.assertExists()
|
||||
ScreenshotTest.takeScreenshot(tag, "Onboarding 1")
|
||||
|
@ -340,6 +341,19 @@ private fun onboardingScreenshots(
|
|||
).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
// Security Warning screen
|
||||
composeTestRule.onNodeWithText(text = resContext.getString(R.string.security_warning_acknowledge)).also {
|
||||
it.assertExists()
|
||||
it.performClick()
|
||||
ScreenshotTest.takeScreenshot(tag, "Security Warning")
|
||||
}
|
||||
composeTestRule.onNodeWithText(
|
||||
text = resContext.getString(R.string.security_warning_confirm),
|
||||
ignoreCase = true
|
||||
).also {
|
||||
it.performClick()
|
||||
}
|
||||
} else {
|
||||
composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_1_header)).also {
|
||||
it.assertExists()
|
||||
|
|
Loading…
Reference in New Issue