[#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
This commit is contained in:
Carter Jernigan 2022-01-26 16:33:02 -05:00 committed by GitHub
parent 52b6382d47
commit 012839841d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 615 additions and 24 deletions

View File

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

View File

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

View File

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

View File

@ -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<WalletViewModel>()
@ -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.
*/

View File

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

View File

@ -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<SecretState.Ready>()
.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)
}
}
}
}
}
}
/**

View File

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

View File

@ -98,7 +98,6 @@ fun PreviewRestoreComplete() {
}
}
@Suppress("UNUSED_PARAMETER")
@Composable
fun RestoreWallet(
completeWordList: Set<String>,

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
<resources>
<string name="seed_title">Backup Wallet</string>
<string name="seed_back_content_description">Back</string>
<string name="seed_header">Your Secret Recovery Phrase</string>
<string name="seed_body">These words represent your funds and the security used to protect them.</string>
<string name="seed_copy">Copy to buffer</string>
</resources>

View File

@ -0,0 +1,10 @@
<resources>
<string name="settings_header">Settings</string>
<string name="settings_back_content_description">Back</string>
<string name="settings_backup">Backup Wallet</string>
<string name="settings_wipe">Wipe Wallet Data</string>
<string name="settings_rescan">Rescan Blockchain</string>
</resources>