From 012839841d611ba9a15a2a33dd4043d572aa3d88 Mon Sep 17 00:00:00 2001 From: Carter Jernigan Date: Wed, 26 Jan 2022 16:33:02 -0500 Subject: [PATCH] [#143] Add settings scaffold (#181) Settings which are not yet implemented have not been included to not introduce non-functional buttons into the UI. Followup issue #38 covers one of the key settings, which is how we'll implement authorization in the app --- ui-lib/build.gradle.kts | 2 + .../z/ecc/ui/screen/seed/view/SeedViewTest.kt | 86 ++++++++++++ .../screen/settings/view/SettingsViewTest.kt | 128 ++++++++++++++++++ .../main/java/cash/z/ecc/ui/MainActivity.kt | 115 +++++++++++++--- .../cash/z/ecc/ui/screen/common/Button.kt | 19 +++ .../screen/home/viewmodel/WalletViewModel.kt | 71 +++++++++- .../onboarding/state/OnboardingState.kt | 4 + .../ecc/ui/screen/restore/view/RestoreView.kt | 1 - .../z/ecc/ui/screen/seed/view/SeedView.kt | 88 ++++++++++++ .../ui/screen/settings/view/SettingsView.kt | 82 +++++++++++ .../main/java/cash/z/ecc/ui/theme/Color.kt | 6 + .../main/java/cash/z/ecc/ui/theme/Theme.kt | 18 ++- .../src/main/res/ui/seed/values/strings.xml | 9 ++ .../main/res/ui/settings/values/strings.xml | 10 ++ 14 files changed, 615 insertions(+), 24 deletions(-) create mode 100644 ui-lib/src/androidTest/java/cash/z/ecc/ui/screen/seed/view/SeedViewTest.kt create mode 100644 ui-lib/src/androidTest/java/cash/z/ecc/ui/screen/settings/view/SettingsViewTest.kt create mode 100644 ui-lib/src/main/java/cash/z/ecc/ui/screen/seed/view/SeedView.kt create mode 100644 ui-lib/src/main/java/cash/z/ecc/ui/screen/settings/view/SettingsView.kt create mode 100644 ui-lib/src/main/res/ui/seed/values/strings.xml create mode 100644 ui-lib/src/main/res/ui/settings/values/strings.xml diff --git a/ui-lib/build.gradle.kts b/ui-lib/build.gradle.kts index e27506bf..c040b958 100644 --- a/ui-lib/build.gradle.kts +++ b/ui-lib/build.gradle.kts @@ -32,6 +32,8 @@ android { "src/main/res/ui/onboarding", "src/main/res/ui/profile", "src/main/res/ui/restore", + "src/main/res/ui/seed", + "src/main/res/ui/settings", "src/main/res/ui/wallet_address" ) ) diff --git a/ui-lib/src/androidTest/java/cash/z/ecc/ui/screen/seed/view/SeedViewTest.kt b/ui-lib/src/androidTest/java/cash/z/ecc/ui/screen/seed/view/SeedViewTest.kt new file mode 100644 index 00000000..959e6be5 --- /dev/null +++ b/ui-lib/src/androidTest/java/cash/z/ecc/ui/screen/seed/view/SeedViewTest.kt @@ -0,0 +1,86 @@ +package cash.z.ecc.ui.screen.seed.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.performScrollTo +import androidx.test.filters.MediumTest +import cash.z.ecc.sdk.fixture.PersistableWalletFixture +import cash.z.ecc.ui.R +import cash.z.ecc.ui.test.getStringResource +import cash.z.ecc.ui.theme.ZcashTheme +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import java.util.concurrent.atomic.AtomicInteger + +@OptIn(ExperimentalCoroutinesApi::class) +class SeedViewTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + @MediumTest + fun back() = runTest { + val testSetup = TestSetup(composeTestRule) + + assertEquals(0, testSetup.getOnBackCount()) + + composeTestRule.onNodeWithContentDescription(getStringResource(R.string.seed_back_content_description)).also { + it.performClick() + } + + assertEquals(1, testSetup.getOnBackCount()) + } + + @Test + @MediumTest + fun copyToClipboard() = runTest { + val testSetup = TestSetup(composeTestRule) + + assertEquals(0, testSetup.getCopyToClipboardCount()) + + composeTestRule.onNodeWithText(getStringResource(R.string.seed_copy)).also { + it.performScrollTo() + it.performClick() + } + + assertEquals(1, testSetup.getCopyToClipboardCount()) + } + + private class TestSetup(private val composeTestRule: ComposeContentTestRule) { + + private var onBackCount = AtomicInteger(0) + private var onCopyToClipboardCount = AtomicInteger(0) + + fun getOnBackCount(): Int { + composeTestRule.waitForIdle() + return onBackCount.get() + } + + fun getCopyToClipboardCount(): Int { + composeTestRule.waitForIdle() + return onCopyToClipboardCount.get() + } + + init { + composeTestRule.setContent { + ZcashTheme { + Seed( + PersistableWalletFixture.new(), + onBack = { + onBackCount.incrementAndGet() + }, + onCopyToClipboard = { + onCopyToClipboardCount.incrementAndGet() + } + ) + } + } + } + } +} diff --git a/ui-lib/src/androidTest/java/cash/z/ecc/ui/screen/settings/view/SettingsViewTest.kt b/ui-lib/src/androidTest/java/cash/z/ecc/ui/screen/settings/view/SettingsViewTest.kt new file mode 100644 index 00000000..8921356a --- /dev/null +++ b/ui-lib/src/androidTest/java/cash/z/ecc/ui/screen/settings/view/SettingsViewTest.kt @@ -0,0 +1,128 @@ +package cash.z.ecc.ui.screen.settings.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.test.filters.MediumTest +import cash.z.ecc.ui.R +import cash.z.ecc.ui.test.getStringResource +import cash.z.ecc.ui.theme.ZcashTheme +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import java.util.concurrent.atomic.AtomicInteger + +@OptIn(ExperimentalCoroutinesApi::class) +class SettingsViewTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + @MediumTest + fun back() = runTest { + val testSetup = TestSetup(composeTestRule) + + assertEquals(0, testSetup.getOnBackCount()) + + composeTestRule.onNodeWithContentDescription(getStringResource(R.string.settings_back_content_description)).also { + it.performClick() + } + + assertEquals(1, testSetup.getOnBackCount()) + } + + @Test + @MediumTest + fun backup() = runTest { + val testSetup = TestSetup(composeTestRule) + + assertEquals(0, testSetup.getBackupCount()) + + composeTestRule.onNodeWithText(getStringResource(R.string.settings_backup)).also { + it.performClick() + } + + assertEquals(1, testSetup.getBackupCount()) + } + + @Test + @MediumTest + fun rescan() = runTest { + val testSetup = TestSetup(composeTestRule) + + assertEquals(0, testSetup.getBackupCount()) + + composeTestRule.onNodeWithText(getStringResource(R.string.settings_rescan)).also { + it.performClick() + } + + assertEquals(1, testSetup.getRescanCount()) + } + + @Test + @MediumTest + fun wipe() = runTest { + val testSetup = TestSetup(composeTestRule) + + assertEquals(0, testSetup.getBackupCount()) + + composeTestRule.onNodeWithText(getStringResource(R.string.settings_wipe)).also { + it.performClick() + } + + assertEquals(1, testSetup.getWipeCount()) + } + + private class TestSetup(private val composeTestRule: ComposeContentTestRule) { + + private var onBackCount = AtomicInteger(0) + private var onBackupCount = AtomicInteger(0) + private var onRescanCount = AtomicInteger(0) + private var onWipeCount = AtomicInteger(0) + + fun getOnBackCount(): Int { + composeTestRule.waitForIdle() + return onBackCount.get() + } + + fun getBackupCount(): Int { + composeTestRule.waitForIdle() + return onBackupCount.get() + } + + fun getRescanCount(): Int { + composeTestRule.waitForIdle() + return onRescanCount.get() + } + + fun getWipeCount(): Int { + composeTestRule.waitForIdle() + return onWipeCount.get() + } + + init { + composeTestRule.setContent { + ZcashTheme { + Settings( + onBack = { + onBackCount.incrementAndGet() + }, + onBackupWallet = { + onBackupCount.incrementAndGet() + }, + onRescanWallet = { + onRescanCount.incrementAndGet() + }, + onWipeWallet = { + onWipeCount.incrementAndGet() + } + ) + } + } + } + } +} diff --git a/ui-lib/src/main/java/cash/z/ecc/ui/MainActivity.kt b/ui-lib/src/main/java/cash/z/ecc/ui/MainActivity.kt index b77021a5..6b94f01a 100644 --- a/ui-lib/src/main/java/cash/z/ecc/ui/MainActivity.kt +++ b/ui-lib/src/main/java/cash/z/ecc/ui/MainActivity.kt @@ -38,6 +38,8 @@ import cash.z.ecc.ui.screen.profile.view.Profile import cash.z.ecc.ui.screen.restore.view.RestoreWallet import cash.z.ecc.ui.screen.restore.viewmodel.CompleteWordSetState import cash.z.ecc.ui.screen.restore.viewmodel.RestoreViewModel +import cash.z.ecc.ui.screen.seed.view.Seed +import cash.z.ecc.ui.screen.settings.view.Settings import cash.z.ecc.ui.screen.wallet_address.view.WalletAddresses import cash.z.ecc.ui.theme.ZcashTheme import cash.z.ecc.ui.util.AndroidApiVersion @@ -48,6 +50,7 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds +@Suppress("TooManyFunctions") class MainActivity : ComponentActivity() { private val walletViewModel by viewModels() @@ -141,12 +144,7 @@ class MainActivity : ComponentActivity() { BackupWallet( persistableWallet, backupViewModel.backupState, backupViewModel.testChoices, onCopyToClipboard = { - val clipboardManager = getSystemService(ClipboardManager::class.java) - val data = ClipData.newPlainText( - getString(R.string.new_wallet_clipboard_tag), - persistableWallet.seedPhrase.joinToString() - ) - clipboardManager.setPrimaryClip(data) + copyToClipboard(applicationContext, persistableWallet) }, onComplete = { walletViewModel.persistBackupComplete() } @@ -200,32 +198,55 @@ class MainActivity : ComponentActivity() { private fun Navigation() { val navController = rememberNavController() - NavHost(navController = navController, startDestination = "home") { - composable("home") { + val home = "home" + val profile = "profile" + val walletAddressDetails = "wallet_address_details" + val settings = "settings" + val seed = "seed" + + NavHost(navController = navController, startDestination = home) { + composable(home) { WrapHome( goScan = {}, - goProfile = { navController.navigate("profile") }, + goProfile = { navController.navigate(profile) }, goSend = {}, goRequest = {} ) } - composable("profile") { + composable(profile) { WrapProfile( onBack = { navController.popBackStack() }, - onAddressDetails = { navController.navigate("wallet_address_details") }, + onAddressDetails = { navController.navigate(walletAddressDetails) }, onAddressBook = { }, - onSettings = { }, - onCoinholderVote = { } - ) { - } + onSettings = { navController.navigate(settings) }, + onCoinholderVote = { }, + onSupport = {} + ) } - composable("wallet_address_details") { + composable(walletAddressDetails) { WrapWalletAddresses( goBack = { navController.popBackStack() } ) } + composable(settings) { + WrapSettings( + goBack = { + navController.popBackStack() + }, + goWalletBackup = { + navController.navigate(seed) + } + ) + } + composable(seed) { + WrapSeed( + goBack = { + navController.popBackStack() + } + ) + } } } @@ -292,12 +313,74 @@ class MainActivity : ComponentActivity() { } } + @Composable + private fun WrapSettings( + goBack: () -> Unit, + goWalletBackup: () -> Unit + ) { + val synchronizer = walletViewModel.synchronizer.collectAsState().value + if (null == synchronizer) { + // Display loading indicator + } else { + Settings( + onBack = goBack, + onBackupWallet = goWalletBackup, + onRescanWallet = { + walletViewModel.rescanBlockchain() + }, onWipeWallet = { + walletViewModel.wipeWallet() + + // If wipe ever becomes an operation to also delete the seed, then we'll also need + // to do the following to clear any retained state from onboarding (only happens if + // occuring during same session as onboarding) + // onboardingViewModel.onboardingState.goToBeginning() + // onboardingViewModel.isImporting.value = false + } + ) + } + } + + @Composable + private fun WrapSeed( + goBack: () -> Unit + ) { + val persistableWallet = run { + val secretState = walletViewModel.secretState.collectAsState().value + if (secretState is SecretState.Ready) { + secretState.persistableWallet + } else { + null + } + } + val synchronizer = walletViewModel.synchronizer.collectAsState().value + if (null == synchronizer || null == persistableWallet) { + // Display loading indicator + } else { + Seed( + persistableWallet = persistableWallet, + onBack = goBack, + onCopyToClipboard = { + copyToClipboard(applicationContext, persistableWallet) + } + ) + } + } + companion object { @VisibleForTesting internal val SPLASH_SCREEN_DELAY = 0.seconds } } +private fun copyToClipboard(context: Context, persistableWallet: PersistableWallet) { + val clipboardManager = context.getSystemService(ClipboardManager::class.java) + val data = ClipData.newPlainText( + context.getString(R.string.new_wallet_clipboard_tag), + persistableWallet.seedPhrase.joinToString() + ) + clipboardManager.setPrimaryClip(data) +} + /** * Pre-fetches fonts on Android N (API 25) and below. */ diff --git a/ui-lib/src/main/java/cash/z/ecc/ui/screen/common/Button.kt b/ui-lib/src/main/java/cash/z/ecc/ui/screen/common/Button.kt index ed250557..d6bef1f7 100644 --- a/ui-lib/src/main/java/cash/z/ecc/ui/screen/common/Button.kt +++ b/ui-lib/src/main/java/cash/z/ecc/ui/screen/common/Button.kt @@ -104,3 +104,22 @@ fun TertiaryButton( Text(style = MaterialTheme.typography.button, text = text, color = ZcashTheme.colors.onTertiary) } } + +@Composable +fun DangerousButton( + onClick: () -> Unit, + text: String, + modifier: Modifier = Modifier +) { + Button( + onClick = onClick, + modifier = modifier.then( + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + ), + colors = buttonColors(backgroundColor = ZcashTheme.colors.dangerous) + ) { + Text(style = MaterialTheme.typography.button, text = text, color = ZcashTheme.colors.onDangerous) + } +} diff --git a/ui-lib/src/main/java/cash/z/ecc/ui/screen/home/viewmodel/WalletViewModel.kt b/ui-lib/src/main/java/cash/z/ecc/ui/screen/home/viewmodel/WalletViewModel.kt index 844f9e15..394cd6ba 100644 --- a/ui-lib/src/main/java/cash/z/ecc/ui/screen/home/viewmodel/WalletViewModel.kt +++ b/ui-lib/src/main/java/cash/z/ecc/ui/screen/home/viewmodel/WalletViewModel.kt @@ -3,6 +3,7 @@ package cash.z.ecc.ui.screen.home.viewmodel import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import cash.z.ecc.android.sdk.Initializer import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.block.CompactBlockProcessor import cash.z.ecc.android.sdk.db.entity.PendingTransaction @@ -10,9 +11,11 @@ import cash.z.ecc.android.sdk.db.entity.Transaction import cash.z.ecc.android.sdk.db.entity.isMined import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess import cash.z.ecc.android.sdk.type.WalletBalance +import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.sdk.SynchronizerCompanion import cash.z.ecc.sdk.model.PersistableWallet import cash.z.ecc.sdk.model.WalletAddresses +import cash.z.ecc.sdk.type.fromResources import cash.z.ecc.ui.common.ANDROID_STATE_FLOW_TIMEOUT_MILLIS import cash.z.ecc.ui.preference.EncryptedPreferenceKeys import cash.z.ecc.ui.preference.EncryptedPreferenceSingleton @@ -46,6 +49,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application) * that they have a consistent ordering. */ private val persistWalletMutex = Mutex() + private val synchronizerMutex = Mutex() /** * A flow of the user's stored wallet. Null indicates that no wallet has been stored. @@ -88,9 +92,11 @@ class WalletViewModel(application: Application) : AndroidViewModel(application) .filterIsInstance() .flatMapConcat { callbackFlow { - val synchronizer = SynchronizerCompanion.load(application, it.persistableWallet) + val synchronizer = synchronizerMutex.withLock { + val synchronizer = SynchronizerCompanion.load(application, it.persistableWallet) - synchronizer.start(viewModelScope) + synchronizer.start(viewModelScope) + } trySend(synchronizer) awaitClose { @@ -183,6 +189,67 @@ class WalletViewModel(application: Application) : AndroidViewModel(application) } } } + + /** + * This method only has an effect if the synchronizer currently is loaded. + */ + fun rescanBlockchain() { + viewModelScope.launch { + synchronizerMutex.withLock { + synchronizer.value?.let { + it.rewindToNearestHeight(it.latestBirthdayHeight, true) + } + } + } + } + + /** + * This asynchronously wipes the wallet state. + * + * This method only has an effect if the synchronizer currently is loaded. + */ + fun wipeWallet() { + /* + * This implementation could perhaps be a little brittle due to needing to stop and start the + * synchronizer. If another client is interacting with the synchronizer at the same time, + * it isn't well defined exactly what the behavior should be. + * + * Possible enhancements to improve this: + * - Hide the synchronizer from clients; prefer to add additional APIs to WalletViewModel + * which delegate to the synchronizer + * - Add a private StateFlow to WalletViewModel to signal internal operations which should + * cancel the synchronizer for other observers. Modify synchronizer flow to use a combine + * operator to check the private stateflow. When initiating a wipe, set that private + * StateFlow to cancel other observers of the synchronizer. + */ + + viewModelScope.launch { + synchronizerMutex.withLock { + synchronizer.value?.let { + // There is a minor race condition here. With the right timing, it is possible + // that the collection of the Synchronizer flow is canceled during an erase. + // In such a situation, the Synchronizer would be restarted at the end of + // this method even though it shouldn't. Overall it shouldn't be too harmful, + // since the viewModelScope would still eventually be canceled. + // By at least checking for referential equality at the end, we can reduce that + // timing gap. + val wasStarted = it.isStarted + if (wasStarted) { + it.stop() + } + + Initializer.erase( + getApplication(), + ZcashNetwork.fromResources(getApplication()) + ) + + if (wasStarted && synchronizer.value === it) { + it.start(viewModelScope) + } + } + } + } + } } /** diff --git a/ui-lib/src/main/java/cash/z/ecc/ui/screen/onboarding/state/OnboardingState.kt b/ui-lib/src/main/java/cash/z/ecc/ui/screen/onboarding/state/OnboardingState.kt index e333ccf6..18f701d1 100644 --- a/ui-lib/src/main/java/cash/z/ecc/ui/screen/onboarding/state/OnboardingState.kt +++ b/ui-lib/src/main/java/cash/z/ecc/ui/screen/onboarding/state/OnboardingState.kt @@ -23,6 +23,10 @@ class OnboardingState(initialState: OnboardingStage = OnboardingStage.values().f mutableState.value = current.value.getPrevious() } + fun goToBeginning() { + mutableState.value = OnboardingStage.values().first() + } + fun goToEnd() { mutableState.value = OnboardingStage.values().last() } diff --git a/ui-lib/src/main/java/cash/z/ecc/ui/screen/restore/view/RestoreView.kt b/ui-lib/src/main/java/cash/z/ecc/ui/screen/restore/view/RestoreView.kt index 38f7a9e0..f1ae7eb1 100644 --- a/ui-lib/src/main/java/cash/z/ecc/ui/screen/restore/view/RestoreView.kt +++ b/ui-lib/src/main/java/cash/z/ecc/ui/screen/restore/view/RestoreView.kt @@ -98,7 +98,6 @@ fun PreviewRestoreComplete() { } } -@Suppress("UNUSED_PARAMETER") @Composable fun RestoreWallet( completeWordList: Set, diff --git a/ui-lib/src/main/java/cash/z/ecc/ui/screen/seed/view/SeedView.kt b/ui-lib/src/main/java/cash/z/ecc/ui/screen/seed/view/SeedView.kt new file mode 100644 index 00000000..d8306c00 --- /dev/null +++ b/ui-lib/src/main/java/cash/z/ecc/ui/screen/seed/view/SeedView.kt @@ -0,0 +1,88 @@ +package cash.z.ecc.ui.screen.seed.view + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import cash.z.ecc.sdk.fixture.PersistableWalletFixture +import cash.z.ecc.sdk.model.PersistableWallet +import cash.z.ecc.ui.R +import cash.z.ecc.ui.screen.common.Body +import cash.z.ecc.ui.screen.common.ChipGrid +import cash.z.ecc.ui.screen.common.GradientSurface +import cash.z.ecc.ui.screen.common.Header +import cash.z.ecc.ui.screen.common.TertiaryButton +import cash.z.ecc.ui.theme.ZcashTheme + +@Preview("Seed") +@Composable +fun PreviewSeed() { + ZcashTheme(darkTheme = true) { + GradientSurface { + Seed( + persistableWallet = PersistableWalletFixture.new(), + onBack = {}, + onCopyToClipboard = {} + ) + } + } +} + +/* + * Note we have some things to determine regarding locking of the secrets for persistableWallet + * (e.g. seed phrase and spending keys) which should require additional authorization to view. + */ +@Composable +fun Seed( + persistableWallet: PersistableWallet, + onBack: () -> Unit, + onCopyToClipboard: () -> Unit +) { + Scaffold(topBar = { + SeedTopAppBar(onBack = onBack) + }) { + SeedMainContent(persistableWallet = persistableWallet, onCopyToClipboard = onCopyToClipboard) + } +} + +@Composable +private fun SeedTopAppBar(onBack: () -> Unit) { + TopAppBar( + title = { Text(text = stringResource(id = R.string.seed_title)) }, + navigationIcon = { + IconButton( + onClick = onBack + ) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = stringResource(R.string.seed_back_content_description) + ) + } + } + ) +} + +@Composable +private fun SeedMainContent( + persistableWallet: PersistableWallet, + onCopyToClipboard: () -> Unit +) { + Column(Modifier.verticalScroll(rememberScrollState())) { + Header(stringResource(R.string.seed_header)) + Body(stringResource(R.string.seed_body)) + + ChipGrid(persistableWallet.seedPhrase.split) + + TertiaryButton(onClick = onCopyToClipboard, text = stringResource(R.string.seed_copy)) + } +} diff --git a/ui-lib/src/main/java/cash/z/ecc/ui/screen/settings/view/SettingsView.kt b/ui-lib/src/main/java/cash/z/ecc/ui/screen/settings/view/SettingsView.kt new file mode 100644 index 00000000..cbd87b78 --- /dev/null +++ b/ui-lib/src/main/java/cash/z/ecc/ui/screen/settings/view/SettingsView.kt @@ -0,0 +1,82 @@ +package cash.z.ecc.ui.screen.settings.view + +import androidx.compose.foundation.layout.Column +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import cash.z.ecc.ui.R +import cash.z.ecc.ui.screen.common.DangerousButton +import cash.z.ecc.ui.screen.common.GradientSurface +import cash.z.ecc.ui.screen.common.PrimaryButton +import cash.z.ecc.ui.screen.common.TertiaryButton +import cash.z.ecc.ui.theme.ZcashTheme + +@Preview("Settings") +@Composable +fun PreviewSettings() { + ZcashTheme(darkTheme = true) { + GradientSurface { + Settings( + onBack = {}, + onBackupWallet = {}, + onWipeWallet = {}, + onRescanWallet = {} + ) + } + } +} + +@Composable +fun Settings( + onBack: () -> Unit, + onBackupWallet: () -> Unit, + onWipeWallet: () -> Unit, + onRescanWallet: () -> Unit, +) { + Scaffold(topBar = { + SettingsTopAppBar(onBack = onBack) + }) { + SettingsMainContent( + onBackupWallet = onBackupWallet, + onWipeWallet = onWipeWallet, + onRescanWallet = onRescanWallet + ) + } +} + +@Composable +private fun SettingsTopAppBar(onBack: () -> Unit) { + TopAppBar( + title = { Text(text = stringResource(id = R.string.settings_header)) }, + navigationIcon = { + IconButton( + onClick = onBack + ) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = stringResource(R.string.settings_back_content_description) + ) + } + } + ) +} + +@Composable +private fun SettingsMainContent( + onBackupWallet: () -> Unit, + onWipeWallet: () -> Unit, + onRescanWallet: () -> Unit +) { + Column { + PrimaryButton(onClick = onBackupWallet, text = stringResource(id = R.string.settings_backup)) + DangerousButton(onClick = onWipeWallet, text = stringResource(id = R.string.settings_wipe)) + TertiaryButton(onClick = onRescanWallet, text = stringResource(id = R.string.settings_rescan)) + } +} diff --git a/ui-lib/src/main/java/cash/z/ecc/ui/theme/Color.kt b/ui-lib/src/main/java/cash/z/ecc/ui/theme/Color.kt index 83c4afdd..aa12dd5d 100644 --- a/ui-lib/src/main/java/cash/z/ecc/ui/theme/Color.kt +++ b/ui-lib/src/main/java/cash/z/ecc/ui/theme/Color.kt @@ -47,6 +47,9 @@ object Dark { val addressHighlightSapling = Color(0xFF1BBFF6) val addressHighlightTransparent = Color(0xFF97999A) val addressHighlightViewing = Color(0xFF504062) + + val dangerous = Color(0xFFEC0008) + val onDangerous = Color(0xFFFFFFFF) } object Light { @@ -93,4 +96,7 @@ object Light { val addressHighlightSapling = Color(0xFF1BBFF6) val addressHighlightTransparent = Color(0xFF97999A) val addressHighlightViewing = Color(0xFF504062) + + val dangerous = Color(0xFFEC0008) + val onDangerous = Color(0xFFFFFFFF) } diff --git a/ui-lib/src/main/java/cash/z/ecc/ui/theme/Theme.kt b/ui-lib/src/main/java/cash/z/ecc/ui/theme/Theme.kt index e3106334..75d49d4b 100644 --- a/ui-lib/src/main/java/cash/z/ecc/ui/theme/Theme.kt +++ b/ui-lib/src/main/java/cash/z/ecc/ui/theme/Theme.kt @@ -19,7 +19,7 @@ private val DarkColorPalette = darkColors( surface = Dark.backgroundStart, onSurface = Dark.textBodyOnBackground, background = Dark.backgroundStart, - onBackground = Dark.textBodyOnBackground + onBackground = Dark.textBodyOnBackground, ) private val LightColorPalette = lightColors( @@ -51,7 +51,9 @@ data class ExtendedColors( val addressHighlightUnified: Color, val addressHighlightSapling: Color, val addressHighlightTransparent: Color, - val addressHighlightViewing: Color + val addressHighlightViewing: Color, + val dangerous: Color, + val onDangerous: Color ) { @Composable fun surfaceGradient() = Brush.verticalGradient( @@ -79,7 +81,9 @@ val DarkExtendedColorPalette = ExtendedColors( addressHighlightUnified = Dark.addressHighlightUnified, addressHighlightSapling = Dark.addressHighlightSapling, addressHighlightTransparent = Dark.addressHighlightTransparent, - addressHighlightViewing = Dark.addressHighlightViewing + addressHighlightViewing = Dark.addressHighlightViewing, + dangerous = Dark.dangerous, + onDangerous = Dark.onDangerous ) val LightExtendedColorPalette = ExtendedColors( @@ -99,7 +103,9 @@ val LightExtendedColorPalette = ExtendedColors( addressHighlightUnified = Light.addressHighlightUnified, addressHighlightSapling = Light.addressHighlightSapling, addressHighlightTransparent = Light.addressHighlightTransparent, - addressHighlightViewing = Light.addressHighlightViewing + addressHighlightViewing = Light.addressHighlightViewing, + dangerous = Light.dangerous, + onDangerous = Light.onDangerous ) val LocalExtendedColors = staticCompositionLocalOf { @@ -120,7 +126,9 @@ val LocalExtendedColors = staticCompositionLocalOf { addressHighlightUnified = Color.Unspecified, addressHighlightSapling = Color.Unspecified, addressHighlightTransparent = Color.Unspecified, - addressHighlightViewing = Color.Unspecified + addressHighlightViewing = Color.Unspecified, + dangerous = Color.Unspecified, + onDangerous = Color.Unspecified ) } diff --git a/ui-lib/src/main/res/ui/seed/values/strings.xml b/ui-lib/src/main/res/ui/seed/values/strings.xml new file mode 100644 index 00000000..64b5aa4c --- /dev/null +++ b/ui-lib/src/main/res/ui/seed/values/strings.xml @@ -0,0 +1,9 @@ + + Backup Wallet + Back + + Your Secret Recovery Phrase + These words represent your funds and the security used to protect them. + Copy to buffer + + diff --git a/ui-lib/src/main/res/ui/settings/values/strings.xml b/ui-lib/src/main/res/ui/settings/values/strings.xml new file mode 100644 index 00000000..1066ca6f --- /dev/null +++ b/ui-lib/src/main/res/ui/settings/values/strings.xml @@ -0,0 +1,10 @@ + + Settings + Back + + Backup Wallet + Wipe Wallet Data + + Rescan Blockchain + +