Merge pull request #207 from zcash/feature/localization-release

Feature/localization release
This commit is contained in:
Kevin Gorham 2020-10-09 13:28:08 -04:00 committed by GitHub
commit 672dfc7d47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 1875 additions and 865 deletions

View File

@ -1,6 +1,16 @@
Change Log
==========
Version 1.0.0-alpha37 *(2020-10-07)*
------------------------------------
- New: Localization in 5 languages Russian, Italian, Spanish, Chinese and Korean.
- New: Store and sync using just the ViewingKey.
- New: Added QA build flavor for better testing.
- New: Ability to change servers (thanks @Nighthawk!)
- Fix: Critical bug in 3rd-party secure storage library impacting large strings.
- Fix: Devices without PIN can use the wallet again.
- Fix: Developer logs now work on all devices.
Version 1.0.0-alpha34 *(2020-08-28)*
------------------------------------
- New: Implemented transaction detail view.

View File

@ -9,7 +9,7 @@ apply plugin: 'com.google.gms.google-services'
archivesBaseName = 'zcash-android-wallet'
group = 'cash.z.ecc.android'
version = '1.0.0-alpha34'
version = Deps.versionName
android {
ndkVersion "21.1.6352462"
@ -17,16 +17,16 @@ android {
buildToolsVersion Deps.buildToolsVersion
viewBinding.enabled = true
defaultConfig {
applicationId 'cash.z.ecc.android'
applicationId Deps.packageName
minSdkVersion Deps.minSdkVersion
targetSdkVersion Deps.targetSdkVersion
versionCode = 1_00_00_034
// last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX) dev(9XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
versionName = "$version"
versionCode = Deps.versionCode
versionName = Deps.versionName
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
testInstrumentationRunnerArguments clearPackageData: 'true'
multiDexEnabled true
resValue 'string', 'bugsnag_api_key', "${(project.findProperty('BUGSNAG_API_KEY') ?: System.getenv('BUGSNAG_API_KEY')) ?: ''}"
resValue 'string', 'file_authority', "${Deps.packageName}.fileprovider"
// this setting allows using color resources in vector drawables, rather than hardcoded values (note: only works when minApi is 21)
// per https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.VectorDrawablesOptions.html: If set to an empty collection, all special handling of vector drawables will be disabled.
@ -68,10 +68,15 @@ android {
useProguard false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
// mock {
// initWith debug
// matchingFallbacks = ['debug', 'release', 'zcashtestnet']
// }
// builds for testing only in the wallet team, typically unfinished features
// this flavor can be installed alongside the others
qa {
initWith debug
debuggable true
applicationIdSuffix ".internal"
resValue 'string', 'file_authority', "${Deps.packageName}.internal.fileprovider"
matchingFallbacks = ['debug']
}
}
compileOptions {
@ -80,6 +85,9 @@ android {
}
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs += "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
// freeCompilerArgs += "-Xopt-in=kotlinx.coroutines.ObsoleteCoroutinesApi"
// freeCompilerArgs += "-Xopt-in=kotlinx.coroutines.FlowPreview"
}
kapt {
arguments {
@ -94,7 +102,10 @@ android {
}
applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = "$archivesBaseName-v${defaultConfig.versionName}-${variant.buildType.name}.apk"
if (variant.buildType.name == "qa") {
it.versionNameOverride = "${Deps.versionName}-QA"
}
outputFileName = "$archivesBaseName-v${Deps.versionName}-${variant.buildType.name}.apk"
}
}
}
@ -165,8 +176,12 @@ dependencies {
testImplementation Deps.Test.COROUTINES_TEST
testImplementation Deps.Test.JUNIT
testImplementation Deps.Test.MOKITO
testImplementation Deps.Test.MOKITO_KOTLIN
testImplementation Deps.Test.COROUTINES_TEST
testImplementation Deps.Zcash.Sdk.TESTNET
androidTestImplementation Deps.Test.Android.JUNIT
androidTestImplementation Deps.Test.Android.ESPRESSO
}
defaultTasks 'clean', 'installZcashmainnetRelease'
defaultTasks 'clean', 'assembleZcashmainnetRelease'

View File

@ -63,6 +63,35 @@
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:000000000000:android:8888888888888888888888",
"android_client_info": {
"package_name": "cash.z.ecc.android.internal"
}
},
"oauth_client": [
{
"client_id": "dummy.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "dummy"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "dummy.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"

View File

@ -0,0 +1,149 @@
package cash.z.ecc.android.integration
import cash.z.ecc.android.ext.WalletZecFormmatter
import org.junit.Assert
import org.junit.Before
import org.junit.Test
class ConversionsTest {
// val formatter: WalletZecFormmatter = WalletZecFormmatter()
@Before
fun setUp() {
}
@Test
fun testToZatoshi() {
val input = "1"
val result = WalletZecFormmatter.toZatoshi(input)
Assert.assertEquals(100_000_000L, result)
}
@Test
fun testToZecString_short() {
val input = 112_340_000L
val result = WalletZecFormmatter.toZecStringShort(input)
Assert.assertEquals("1.123", result)
}
@Test
fun testToZecString_shortRoundUp() {
val input = 112_355_600L
val result = WalletZecFormmatter.toZecStringShort(input)
Assert.assertEquals("1.124", result)
}
@Test
fun testToZecString_shortRoundDown() {
val input = 112_349_999L
val result = WalletZecFormmatter.toZecStringShort(input)
Assert.assertEquals("1.123", result)
}
@Test
fun testToZecString_shortRoundHalfEven() {
val input = 112_250_000L
val result = WalletZecFormmatter.toZecStringShort(input)
Assert.assertEquals("1.122", result)
}
@Test
fun testToZecString_shortRoundHalfOdd() {
val input = 112_350_000L
val result = WalletZecFormmatter.toZecStringShort(input)
Assert.assertEquals("1.124", result)
}
@Test
fun testToBigDecimal_noCommas() {
val input = "1000"
val result = WalletZecFormmatter.toBigDecimal(input)!!
Assert.assertEquals(1000, result.longValueExact())
}
@Test
fun testToBigDecimal_thousandComma() {
val input = "1,000"
val result = WalletZecFormmatter.toBigDecimal(input)!!
Assert.assertEquals(1000, result.longValueExact())
}
@Test
fun testToBigDecimal_thousandCommaWithDecimal() {
val input = "1,000.00"
val result = WalletZecFormmatter.toBigDecimal(input)!!
Assert.assertEquals(1000, result.longValueExact())
}
@Test
fun testToBigDecimal_oneDecimal() {
val input = "1.000"
val result = WalletZecFormmatter.toBigDecimal(input)!!
Assert.assertEquals(1, result.longValueExact())
}
@Test
fun testToBigDecimal_thousandWithThinSpace() {
val input = "1000"
val result = WalletZecFormmatter.toBigDecimal(input)!!
Assert.assertEquals(1000, result.longValueExact())
}
@Test
fun testToBigDecimal_oneWithThinSpace() {
val input = "1.000000"
val result = WalletZecFormmatter.toBigDecimal(input)!!
Assert.assertEquals(1, result.longValueExact())
}
@Test
fun testToBigDecimal_oneDecimalWithComma() {
val input = "1.000,00"
val result = WalletZecFormmatter.toBigDecimal(input)!!
Assert.assertEquals(1, result.longValueExact())
}
@Test
fun testToZecString_full() {
val input = 112_341_123L
val result = WalletZecFormmatter.toZecStringFull(input)
Assert.assertEquals("1.12341123", result)
}
@Test
fun testToZecString_fullRoundUp() {
val input = 112_355_678L
val result = WalletZecFormmatter.toZecStringFull(input)
Assert.assertEquals("1.12355678", result)
}
@Test
fun testToZecString_fullRoundDown() {
val input = 112_349_999L
val result = WalletZecFormmatter.toZecStringFull(input)
Assert.assertEquals("1.12349999", result)
}
@Test
fun testToZecString_fullRoundHalfEven() {
val input = 112_250_009L
val result = WalletZecFormmatter.toZecStringFull(input)
Assert.assertEquals("1.12250009", result)
}
@Test
fun testToZecString_fullRoundHalfOdd() {
val input = 112_350_004L
val result = WalletZecFormmatter.toZecStringFull(input)
Assert.assertEquals("1.12350004", result)
}
}

View File

@ -81,8 +81,8 @@ class IntegrationTest {
@Test
fun testAddress() {
val seed = mnemonics.toSeed(phrase.toCharArray())
val initializer = Initializer(appContext).apply {
new(seed, Initializer.DefaultBirthdayStore(appContext).newWalletBirthday, overwrite = true)
val initializer = Initializer(appContext) { config ->
config.new(seed)
}
assertEquals(
"Generated incorrect z-address!",

View File

@ -0,0 +1,65 @@
package cash.z.ecc.android.integration
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.lockbox.LockBox
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
class LockBoxTest {
lateinit var lockBox: LockBox
lateinit var appContext: Context
private val hex = ('a'..'f') + ('0'..'9')
private val iterations = 50
@Before
fun setUp() {
appContext = InstrumentationRegistry.getInstrumentation().targetContext
lockBox = LockBox(appContext)
lockBox.clear()
}
@Test
fun testLongString() {
var successCount = 0
repeat(iterations) {
val sampleHex = List(500) { hex.random() }.joinToString("")
lockBox["longStr"] = sampleHex
val actual: String = lockBox["longStr"]!!
if(sampleHex == actual) successCount++
lockBox.clear()
}
assertEquals(iterations, successCount)
}
@Test
fun testShortString() {
var successCount = 0
repeat(iterations) {
val sampleHex = List(50) { hex.random() }.joinToString("")
lockBox["shortStr"] = sampleHex
val actual: String = lockBox["shortStr"]!!
if(sampleHex == actual) successCount++
lockBox.clear()
}
assertEquals(iterations, successCount)
}
@Test
fun testGiantString() {
var successCount = 0
repeat(iterations) {
val sampleHex = List(2500) { hex.random() }.joinToString("")
lockBox["giantStr"] = sampleHex
val actual: String = lockBox["giantStr"]!!
if(sampleHex == actual) successCount++
lockBox.clear()
}
assertEquals(iterations, successCount)
}
}

View File

@ -22,7 +22,7 @@
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="cash.z.ecc.android.fileprovider"
android:authorities="@string/file_authority"
android:exported="false"
android:grantUriPermissions="true"
android:writePermission="true">

View File

@ -10,6 +10,7 @@ import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.ext.SilentTwig
import cash.z.ecc.android.sdk.ext.TroubleshootingTwig
import cash.z.ecc.android.sdk.ext.Twig
import cash.z.ecc.android.ui.util.DebugFileTwig
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet
@ -53,7 +54,7 @@ class AppModule {
): FeedbackCoordinator {
return prefs.getBoolean(Const.Pref.FEEDBACK_ENABLED).let { isEnabled ->
// observe nothing unless feedback is enabled
Twig.plant(if (isEnabled) TroubleshootingTwig() else SilentTwig())
Twig.plant(if (isEnabled) DebugFileTwig() else SilentTwig())
FeedbackCoordinator(feedback, if (isEnabled) defaultObservers else setOf())
}
}

View File

@ -2,7 +2,7 @@ package cash.z.ecc.android.ext
object Const {
/**
* Named objects for Dependency Injection
* Named objects for Dependency Injection.
*/
object Name {
/** application data other than cryptographic keys */
@ -12,7 +12,7 @@ object Const {
}
/**
* App preference key names
* App preference key names.
*/
object Pref {
const val FIRST_USE_VIEW_TX = "const.pref.first_use_view_tx"
@ -21,6 +21,21 @@ object Const {
const val SERVER_PORT = "const.pref.server_port"
}
/**
* Constants used for wallet backup.
*/
object Backup {
const val SEED = "cash.z.ecc.android.SEED"
const val SEED_PHRASE = "cash.z.ecc.android.SEED_PHRASE"
const val HAS_SEED = "cash.z.ecc.android.HAS_SEED"
const val HAS_SEED_PHRASE = "cash.z.ecc.android.HAS_SEED_PHRASE"
const val HAS_BACKUP = "cash.z.ecc.android.HAS_BACKUP"
// Config
const val VIEWING_KEY = "cash.z.ecc.android.VIEWING_KEY"
const val BIRTHDAY_HEIGHT = "cash.z.ecc.android.BIRTHDAY_HEIGHT"
}
/**
* Default values to use application-wide. Ideally, this set of values should remain very short.
*/

View File

@ -1,5 +1,8 @@
package cash.z.ecc.android.ext
import cash.z.ecc.android.ext.ConversionsUniform.FULL_FORMATTER
import cash.z.ecc.android.ext.ConversionsUniform.LONG_SCALE
import cash.z.ecc.android.ext.ConversionsUniform.SHORT_FORMATTER
import cash.z.ecc.android.sdk.ext.Conversions
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.convertZatoshiToZec
@ -11,123 +14,66 @@ import java.text.DecimalFormat
import java.text.NumberFormat
import java.util.*
/**
* Do the necessary conversions in one place
*
* "1.234" -> to zatoshi
* (zecStringToZatoshi)
* String.toZatoshi()
*
* 123123 -> to "1.2132"
* (zatoshiToZecString)
* Long.toZecString()
*
*/
object ConversionsUniform {
var ONE_ZEC_IN_ZATOSHI = BigDecimal(ZcashSdk.ZATOSHI_PER_ZEC, MathContext.DECIMAL128)
var ZEC_FORMATTER = (NumberFormat.getNumberInstance(Locale("en", "UK")) as DecimalFormat).apply {
applyPattern("###.##")
roundingMode = RoundingMode.DOWN
maximumFractionDigits = 6
minimumFractionDigits = 0
val ONE_ZEC_IN_ZATOSHI = BigDecimal(ZcashSdk.ZATOSHI_PER_ZEC, MathContext.DECIMAL128)
val LONG_SCALE = 8
val SHORT_SCALE = 8
val SHORT_FORMATTER = from(SHORT_SCALE)
val FULL_FORMATTER = from(LONG_SCALE)
val roundingMode = RoundingMode.HALF_EVEN
private fun from(maxDecimals: Int = 8, minDecimals: Int = 0) = (NumberFormat.getNumberInstance(Locale("en", "USA")) as DecimalFormat).apply {
// applyPattern("###.##")
isParseBigDecimal = true
roundingMode = roundingMode
maximumFractionDigits = maxDecimals
minimumFractionDigits = minDecimals
minimumIntegerDigits = 1
}
}
/**
* Format a Zatoshi value into ZEC with the given number of digits, represented as a string.
* Start with Zatoshi -> End with ZEC.
*
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because ZEC is
* better than USD.
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
*
* @return this Zatoshi value represented as ZEC, in a string with at least [minDecimals] and at
* most [maxDecimals]
*/
inline fun Long?.convertZatoshiToZecStringUniform(
maxDecimals: Int = ConversionsUniform.ZEC_FORMATTER.maximumFractionDigits,
minDecimals: Int = ConversionsUniform.ZEC_FORMATTER.minimumFractionDigits
): String {
return currencyFormatterUniform(maxDecimals, minDecimals).format(this.convertZatoshiToZec(maxDecimals))
}
/**
* Format a ZEC value into ZEC with the given number of digits, represented as a string.
* Start with ZEC -> End with ZEC.
*
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because ZEC is
* better when right.
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
*
* @return this Double ZEC value represented as a string with at least [minDecimals] and at most
* [maxDecimals].
*/
inline fun Double?.toZecStringUniform(
maxDecimals: Int = ConversionsUniform.ZEC_FORMATTER.maximumFractionDigits,
minDecimals: Int = ConversionsUniform.ZEC_FORMATTER.minimumFractionDigits
): String {
return currencyFormatterUniform(maxDecimals, minDecimals).format(this.toZec(maxDecimals))
}
/**
* Format a Zatoshi value into ZEC with the given number of decimal places, represented as a string.
* Start with ZeC -> End with ZEC.
*
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because ZEC is
* better than bread.
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
*
* @return this BigDecimal ZEC value represented as a string with at least [minDecimals] and at most
* [maxDecimals].
*/
inline fun BigDecimal?.toZecStringUniform(
maxDecimals: Int = ConversionsUniform.ZEC_FORMATTER.maximumFractionDigits,
minDecimals: Int = ConversionsUniform.ZEC_FORMATTER.minimumFractionDigits
): String {
return currencyFormatterUniform(maxDecimals, minDecimals).format(this.toZecUniform(maxDecimals))
}
/**
* Format a Double ZEC value as a BigDecimal ZEC value, right-padded to the given number of fraction
* digits.
* Start with ZEC -> End with ZEC.
*
* @param decimals the scale to use for the resulting BigDecimal.
*
* @return this Double ZEC value converted into a BigDecimal, with the proper rounding mode for use
* with other formatting functions.
*/
inline fun Double?.toZec(decimals: Int = ConversionsUniform.ZEC_FORMATTER.maximumFractionDigits): BigDecimal {
return BigDecimal(this?.toString() ?: "0.0", MathContext.DECIMAL128).setScale(
decimals,
ConversionsUniform.ZEC_FORMATTER.roundingMode
)
}
/**
* Format a BigDecimal ZEC value as a BigDecimal ZEC value, right-padded to the given number of
* fraction digits.
* Start with ZEC -> End with ZEC.
*
* @param decimals the scale to use for the resulting BigDecimal.
*
* @return this BigDecimal ZEC adjusted to the default scale and rounding mode.
*/
inline fun BigDecimal?.toZecUniform(decimals: Int = ConversionsUniform.ZEC_FORMATTER.maximumFractionDigits): BigDecimal {
return (this ?: BigDecimal.ZERO).setScale(decimals, ConversionsUniform.ZEC_FORMATTER.roundingMode)
}
/**
* Create a number formatter for use with converting currency to strings. This probably isn't needed
* externally since the other formatting functions leverage this, instead. Leverages the default
* rounding mode for ZEC found in ZEC_FORMATTER.
*
* @param maxDecimals the number of decimal places to use in the format. Default is 6 because ZEC is
* glorious.
* @param minDecimals the minimum number of digits to allow to the right of the decimal.
*
* @return a currency formatter, appropriate for the default locale.
*/
inline fun currencyFormatterUniform(maxDecimals: Int, minDecimals: Int): DecimalFormat {
return (ConversionsUniform.ZEC_FORMATTER.clone() as DecimalFormat).apply {
maximumFractionDigits = maxDecimals
minimumFractionDigits = minDecimals
object WalletZecFormmatter {
fun toZatoshi(zecString: String): Long? {
return toBigDecimal(zecString)?.multiply(Conversions.ONE_ZEC_IN_ZATOSHI, MathContext.DECIMAL128)?.toLong()
}
fun toZecStringShort(zatoshi: Long?): String {
return SHORT_FORMATTER.format((zatoshi ?: 0).toZec())
}
fun toZecStringFull(zatoshi: Long?): String {
return formatFull((zatoshi ?: 0).toZec())
}
fun formatFull(zec: BigDecimal): String {
return FULL_FORMATTER.format(zec)
}
fun toBigDecimal(zecString: String?): BigDecimal? {
if (zecString.isNullOrEmpty()) return BigDecimal.ZERO
return try {
// ignore commas and whitespace
var sanitizedInput = zecString.filter { it.isDigit() or (it == '.') }
BigDecimal.ZERO.max(FULL_FORMATTER.parse(sanitizedInput) as BigDecimal)
} catch (t: Throwable) {
return null
}
}
}
/**
* Checks if the decimal separator is the last symbol
*/
inline fun String.endsWithDecimalSeparator(): Boolean {
return this.endsWith(ConversionsUniform.ZEC_FORMATTER.decimalFormatSymbols.toString())
// convert a zatoshi value to ZEC as a BigDecimal
private fun Long?.toZec(): BigDecimal =
BigDecimal(this ?: 0L, MathContext.DECIMAL128)
.divide(ConversionsUniform.ONE_ZEC_IN_ZATOSHI)
.setScale(LONG_SCALE, ConversionsUniform.roundingMode)
}

View File

@ -3,26 +3,22 @@ package cash.z.ecc.android.ext
import android.app.ActivityManager
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.provider.Settings
import android.view.View
import androidx.core.content.getSystemService
import cash.z.ecc.android.sdk.exception.LightWalletException
import cash.z.ecc.android.ui.MainActivity
import cash.z.ecc.android.R
import com.google.android.material.dialog.MaterialAlertDialogBuilder
fun Context.showClearDataConfirmation(onDismiss: () -> Unit = {}, onCancel: () -> Unit = {}): Dialog {
return MaterialAlertDialogBuilder(this)
.setTitle("Nuke Wallet?")
.setMessage("WARNING: Potential Loss of Funds\n\nClearing all wallet data and can result in a loss of funds, if you cannot locate your correct seed phrase.\n\nPlease confirm that you have your 24-word seed phrase available before proceeding.")
.setTitle(R.string.dialog_nuke_wallet_title)
.setMessage(R.string.dialog_nuke_wallet_message)
.setCancelable(false)
.setPositiveButton("Cancel") { dialog, _ ->
.setPositiveButton(R.string.dialog_nuke_wallet_button_positive) { dialog, _ ->
dialog.dismiss()
onDismiss()
onCancel()
}
.setNegativeButton("Erase Wallet") { dialog, _ ->
.setNegativeButton(R.string.dialog_nuke_wallet_button_negative) { dialog, _ ->
dialog.dismiss()
onDismiss()
getSystemService<ActivityManager>()?.clearApplicationUserData()
@ -32,15 +28,15 @@ fun Context.showClearDataConfirmation(onDismiss: () -> Unit = {}, onCancel: () -
fun Context.showUninitializedError(error: Throwable? = null, onDismiss: () -> Unit = {}): Dialog {
return MaterialAlertDialogBuilder(this)
.setTitle("Wallet Improperly Initialized")
.setMessage("This wallet has not been initialized correctly! Perhaps an error occurred during install.\n\nThis can be fixed with a reset. First, locate your backup seed phrase, then CLEAR DATA and reimport it.")
.setTitle(R.string.dialog_error_uninitialized_title)
.setMessage(R.string.dialog_error_uninitialized_message)
.setCancelable(false)
.setPositiveButton("Exit") { dialog, _ ->
.setPositiveButton(getString(R.string.dialog_error_uninitialized_button_positive)) { dialog, _ ->
dialog.dismiss()
onDismiss()
if (error != null) throw error
}
.setNegativeButton("Clear Data") { dialog, _ ->
.setNegativeButton(getString(R.string.dialog_error_uninitialized_button_negative)) { dialog, _ ->
showClearDataConfirmation(onDismiss, onCancel = {
// do not let the user back into the app because we cannot recover from this case
showUninitializedError(error, onDismiss)
@ -51,10 +47,10 @@ fun Context.showUninitializedError(error: Throwable? = null, onDismiss: () -> Un
fun Context.showInvalidSeedPhraseError(error: Throwable? = null, onDismiss: () -> Unit = {}): Dialog {
return MaterialAlertDialogBuilder(this)
.setTitle("Oops! Invalid Seed Phrase")
.setMessage("That seed phrase appears to be invalid! Please double-check it and try again.\n\n${error?.message ?: ""}")
.setTitle(R.string.dialog_error_invalid_seed_phrase_title)
.setMessage(getString(R.string.dialog_error_invalid_seed_phrase_message, error?.message ?: ""))
.setCancelable(false)
.setPositiveButton("Retry") { dialog, _ ->
.setPositiveButton(getString(R.string.dialog_error_invalid_seed_phrase_button_positive)) { dialog, _ ->
dialog.dismiss()
onDismiss()
}
@ -68,14 +64,14 @@ fun Context.showScanFailure(error: Throwable?, onCancel: () -> Unit = {}, onDism
"${error.message}${if (error.cause != null) "\n\nCaused by: ${error.cause}" else ""}"
}
return MaterialAlertDialogBuilder(this)
.setTitle("Scan Failure")
.setTitle(R.string.dialog_error_scan_failure_title)
.setMessage(message)
.setCancelable(true)
.setPositiveButton("Retry") { d, _ ->
.setPositiveButton(R.string.dialog_error_scan_failure_button_positive) { d, _ ->
d.dismiss()
onDismiss()
}
.setNegativeButton("Ignore") { d, _ ->
.setNegativeButton(R.string.dialog_error_scan_failure_button_negative) { d, _ ->
d.dismiss()
onCancel()
onDismiss()
@ -85,14 +81,14 @@ fun Context.showScanFailure(error: Throwable?, onCancel: () -> Unit = {}, onDism
fun Context.showCriticalProcessorError(error: Throwable?, onRetry: () -> Unit = {}): Dialog {
return MaterialAlertDialogBuilder(this)
.setTitle("Processor Error")
.setMessage(error?.message ?: "Critical error while processing blocks!")
.setTitle(R.string.dialog_error_processor_critical_title)
.setMessage(error?.message ?: getString(R.string.dialog_error_processor_critical_message))
.setCancelable(false)
.setPositiveButton("Retry") { d, _ ->
.setPositiveButton(R.string.dialog_error_processor_critical_button_positive) { d, _ ->
d.dismiss()
onRetry()
}
.setNegativeButton("Exit") { dialog, _ ->
.setNegativeButton(R.string.dialog_error_processor_critical_button_negative) { dialog, _ ->
dialog.dismiss()
throw error ?: RuntimeException("Critical error while processing blocks and the user chose to exit.")
}
@ -101,26 +97,26 @@ fun Context.showCriticalProcessorError(error: Throwable?, onRetry: () -> Unit =
fun Context.showUpdateServerCriticalError(userFacingMessage: String, onConfirm: () -> Unit = {}): Dialog {
return MaterialAlertDialogBuilder(this)
.setTitle("Failed to Change Server")
.setTitle(R.string.dialog_error_change_server_title)
.setMessage(userFacingMessage)
.setCancelable(false)
.setPositiveButton("Ok") { d, _ ->
.setPositiveButton(R.string.dialog_error_change_server_button_positive) { d, _ ->
d.dismiss()
onConfirm()
}
.show()
}
fun Context.showUpdateServerDialog(positiveText: String = "Update", onCancel: () -> Unit = {}, onUpdate: () -> Unit = {}): Dialog {
fun Context.showUpdateServerDialog(positiveResId: Int = R.string.dialog_modify_server_button_positive, onCancel: () -> Unit = {}, onUpdate: () -> Unit = {}): Dialog {
return MaterialAlertDialogBuilder(this)
.setTitle("Modify Lightwalletd Server?")
.setMessage("WARNING: Entering an invalid or untrusted server might result in misconfiguration or loss of funds!")
.setTitle(R.string.dialog_modify_server_title)
.setMessage(R.string.dialog_modify_server_message)
.setCancelable(false)
.setPositiveButton(positiveText) { dialog, _ ->
.setPositiveButton(positiveResId) { dialog, _ ->
dialog.dismiss()
onUpdate()
}
.setNegativeButton("Cancel") { dialog, _ ->
.setNegativeButton(R.string.dialog_modify_server_button_negative) { dialog, _ ->
dialog.dismiss()
onCancel
}

View File

@ -42,7 +42,7 @@ inline fun EditText.limitDecimalPlaces(max: Int) {
if (editText.selectionStart == editText.selectionEnd && editText.selectionStart != textStr.length) {
textStr = previousValue
} else {
textStr = number.toZecStringUniform(8)
textStr = WalletZecFormmatter.formatFull(number)
}
}

View File

@ -1,8 +1,15 @@
package cash.z.ecc.android.ext
import android.content.Context
import android.os.Build
import androidx.fragment.app.Fragment
import cash.z.ecc.android.sdk.ext.Bush
import cash.z.ecc.android.sdk.ext.CompositeTwig
import cash.z.ecc.android.sdk.ext.Twig
import cash.z.ecc.android.sdk.ext.twig
import java.util.*
fun Boolean.asString(ifTrue: String = "", ifFalse: String = "") = if(this) ifTrue else ifFalse
fun Boolean.asString(ifTrue: String = "", ifFalse: String = "") = if (this) ifTrue else ifFalse
inline fun <R> tryWithWarning(message: String = "", block: () -> R): R? {
return try {
@ -11,4 +18,21 @@ inline fun <R> tryWithWarning(message: String = "", block: () -> R): R? {
twig("WARNING: $message")
null
}
}
inline fun Fragment.locale(): Locale = context?.locale() ?: Locale.getDefault()
inline fun Context.locale(): Locale {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
resources.configuration.locales.get(0)
} else {
//noinspection deprecation
resources.configuration.locale
}
}
// TODO: add this to the SDK and if the trunk is a CompositeTwig, search through there before returning null
inline fun <reified T> Twig.find(): T? {
return if (Bush.trunk::class.java.isAssignableFrom(T::class.java)) Bush.trunk as T
else null
}

View File

@ -19,8 +19,11 @@ internal inline fun @receiver:ColorRes Int.toAppColor(): Int {
/**
* Grab a string from the application resources
*/
internal inline fun @receiver:StringRes Int.toAppString(): String {
return ZcashWalletApp.instance.getString(this)}
internal inline fun @receiver:StringRes Int.toAppString(lowercase: Boolean = false): String {
return ZcashWalletApp.instance.getString(this).let {
if (lowercase) it.toLowerCase() else it
}
}
/**

View File

@ -38,7 +38,9 @@ import cash.z.ecc.android.databinding.DialogFirstUseMessageBinding
import cash.z.ecc.android.di.component.MainActivitySubcomponent
import cash.z.ecc.android.di.component.SynchronizerSubcomponent
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.ext.showCriticalProcessorError
import cash.z.ecc.android.ext.showScanFailure
import cash.z.ecc.android.ext.showUninitializedError
import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.feedback.FeedbackCoordinator
import cash.z.ecc.android.feedback.LaunchMetric
@ -247,6 +249,9 @@ class MainActivity : AppCompatActivity() {
}
else -> {
twig("Warning: failed authentication because $errString [$errorCode]")
// we get here when a device does not have a pin setup, at all
// in the future, notify the user here that they can set a pin on their device for added security
block()
}
}
}
@ -255,7 +260,7 @@ class MainActivity : AppCompatActivity() {
BiometricPrompt(this, ContextCompat.getMainExecutor(this), callback).apply {
authenticate(
BiometricPrompt.PromptInfo.Builder()
.setTitle("Authenticate to Proceed")
.setTitle(getString(R.string.biometric_prompt_title))
.setConfirmationRequired(false)
.setDescription(description)
.setDeviceCredentialAllowed(true)
@ -331,7 +336,7 @@ class MainActivity : AppCompatActivity() {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
fun showSnackbar(message: String, action: String = "OK"): Snackbar {
fun showSnackbar(message: String, action: String = getString(android.R.string.ok)): Snackbar {
return if (snackbar == null) {
val view = findViewById<View>(R.id.main_activity_container)
val snacks = Snackbar
@ -410,7 +415,7 @@ class MainActivity : AppCompatActivity() {
}
private fun onNoCamera() {
showSnackbar("Well, this is awkward. You denied permission for the camera.")
showSnackbar(getString(R.string.camera_permission_denied))
}
// TODO: clean up this error handling
@ -422,15 +427,9 @@ class MainActivity : AppCompatActivity() {
if (dialog == null) {
notified = true
runOnUiThread {
dialog = MaterialAlertDialogBuilder(this)
.setTitle("Wallet Improperly Initialized")
.setMessage("This wallet has not been initialized correctly! Perhaps an error occurred during install.\n\nThis can be fixed with a reset. Please reimport using your backup seed phrase.")
.setCancelable(false)
.setPositiveButton("Exit") { dialog, _ ->
dialog.dismiss()
throw error
}
.show()
dialog = showUninitializedError(error) {
dialog = null
}
}
}
}
@ -438,20 +437,10 @@ class MainActivity : AppCompatActivity() {
if (dialog == null && !ignoreScanFailure) throttle("scanFailure", 20_000L) {
notified = true
runOnUiThread {
dialog = MaterialAlertDialogBuilder(this)
.setTitle("Scan Failure")
.setMessage("${error.message}${if (error.cause != null) "\n\nCaused by: ${error.cause}" else ""}")
.setCancelable(true)
.setPositiveButton("Retry") { d, _ ->
d.dismiss()
dialog = null
}
.setNegativeButton("Ignore") { d, _ ->
d.dismiss()
ignoreScanFailure = true
dialog = null
}
.show()
dialog = showScanFailure(error,
onCancel = { dialog = null },
onDismiss = { dialog = null }
)
}
}
}
@ -462,20 +451,9 @@ class MainActivity : AppCompatActivity() {
if (dialog == null) {
notified = true
runOnUiThread {
dialog = MaterialAlertDialogBuilder(this)
.setTitle("Processor Error")
.setMessage(error?.message ?: "Critical error while processing blocks!")
.setCancelable(false)
.setPositiveButton("Retry") { d, _ ->
d.dismiss()
dialog = null
}
.setNegativeButton("Exit") { dialog, _ ->
dialog.dismiss()
throw error
?: RuntimeException("Critical error while processing blocks and the user chose to exit.")
}
.show()
dialog = showCriticalProcessorError(error) {
dialog = null
}
}
}
}
@ -604,7 +582,7 @@ class MainActivity : AppCompatActivity() {
try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
} catch (t: Throwable) {
Toast.makeText(this, "Failed to open browser.", Toast.LENGTH_LONG).show()
Toast.makeText(this, R.string.error_launch_url, Toast.LENGTH_LONG).show()
twig("Warning: failed to open browser due to $t")
}
}

View File

@ -10,9 +10,7 @@ import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentHistoryBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onClickNavUp
import cash.z.ecc.android.ext.toColoredSpan
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.HISTORY_BACK
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.WalletBalance
@ -44,7 +42,7 @@ class HistoryFragment : BaseFragment<FragmentHistoryBinding>() {
initTransactionUI()
binding.backButtonHitArea.onClickNavUp { tapped(HISTORY_BACK) }
lifecycleScope.launch {
binding.textAddress.text = viewModel.getAddress().toAbbreviatedAddress()
binding.textAddress.text = viewModel.getAddress().toAbbreviatedAddress(10, 10)
}
}
@ -58,12 +56,13 @@ class HistoryFragment : BaseFragment<FragmentHistoryBinding>() {
}
private fun onBalanceUpdated(balance: WalletBalance) {
binding.textBalanceAvailable.text = balance.availableZatoshi.convertZatoshiToZecString()
binding.textBalanceAvailable.text = WalletZecFormmatter.toZecStringShort(balance.availableZatoshi)
val change = (balance.totalZatoshi - balance.availableZatoshi)
binding.textBalanceDescription.apply {
goneIf(change <= 0L)
val changeString = change.convertZatoshiToZecString()
text = "(expecting +$changeString ZEC)".toColoredSpan(R.color.text_light, "+${changeString}")
val changeString = WalletZecFormmatter.toZecStringFull(change)
val expecting = R.string.home_banner_expecting.toAppString(true)
text = "($expecting +$changeString ZEC)".toColoredSpan(R.color.text_light, "+${changeString}")
}
}

View File

@ -18,7 +18,10 @@ import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.ext.*
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.ui.MainActivity
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.util.toUtf8Memo
@ -116,9 +119,9 @@ class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
// TODO: remove logic from sections below and add more fields or extension functions to UiModel
uiModel.confirmation?.let {
subwaySpotConfirmations.visible(); subwayLabelConfirmations.visible();
subwaySpotConfirmations.visible(); subwayLabelConfirmations.visible()
subwayLabelConfirmations.text = it
if (it.equals("confirmed", true)) {
if (it.equals(getString(R.string.transaction_status_confirmed), true)) {
subwayLabelConfirmations.setTextColor(R.color.tx_primary.toAppColor())
} else {
subwayLabelConfirmations.setTextColor(R.color.tx_text_light_dimmed.toAppColor())
@ -196,13 +199,11 @@ class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
else -> null
}
isMined = tx?.minedHeight != null && tx.minedHeight > ZcashSdk.SAPLING_ACTIVATION_HEIGHT
topValue = if (tx == null) "" else "\$${tx?.value.convertZatoshiToZecString()}"
minedHeight = NumberFormat.getNumberInstance(Locale.getDefault()).format(
tx?.minedHeight ?: 0
)
topValue = if (tx == null) "" else "\$${WalletZecFormmatter.toZecStringFull(tx.value)}"
minedHeight = (tx?.minedHeight ?: 0).toString()
val flags =
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR or DateUtils.FORMAT_ABBREV_MONTH
timestamp = if (tx == null) "Details Unavailable" else DateUtils.getRelativeDateTimeString(
timestamp = if (tx == null) getString(R.string.transaction_timestamp_unavailable) else DateUtils.getRelativeDateTimeString(
ZcashWalletApp.instance,
tx.blockTimeInSeconds * 1000,
DateUtils.SECOND_IN_MILLIS,
@ -212,7 +213,7 @@ class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
// memo logic
val txMemo = tx?.memo.toUtf8Memo()
if (!txMemo.isNullOrEmpty()) {
if (!txMemo.isEmpty()) {
memo = txMemo
}
@ -224,18 +225,18 @@ class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
val hasLatestHeight = latestHeight != null && latestHeight > ZcashSdk.SAPLING_ACTIVATION_HEIGHT
if (it.minedHeight > 0 && hasLatestHeight) {
val confirmations = latestHeight!! - it.minedHeight + 1
confirmation = if (confirmations > 10) "Confirmed" else "$confirmations of 10 Confirmations"
confirmation = if (confirmations >= 10) getString(R.string.transaction_status_confirmed) else "$confirmations ${getString(R.string.transaction_status_confirming)}"
} else {
if (!hasLatestHeight && isSufficientlyOld(tx)) {
twig("Warning: could not load latestheight from server to determine confirmations but this transaction is mined and old enough to be considered confirmed")
confirmation = "Confirmed"
confirmation = getString(R.string.transaction_status_confirmed)
} else {
twig("Warning: could not determine confirmation text value so it will be left null!")
confirmation = "Confirmation count temporarily unavailable"
confirmation = getString(R.string.transaction_confirmation_count_unavailable)
}
}
} else {
confirmation = "Pending"
confirmation = getString(R.string.transaction_status_pending)
}
}
@ -244,20 +245,20 @@ class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
// inbound v. outbound values
when (isInbound) {
true -> {
topLabel = "You Received"
bottomLabel = "Total Received"
bottomValue = "\$${tx?.value.convertZatoshiToZecString()}"
topLabel = getString(R.string.transaction_story_inbound)
bottomLabel = getString(R.string.transaction_story_inbound_total)
bottomValue = "\$${WalletZecFormmatter.toZecStringFull(tx?.value)}"
iconRotation = 315f
source = "to your shielded wallet"
source = getString(R.string.transaction_story_to_shielded)
address = mainActivity.extractValidAddress(tx?.memo.toUtf8Memo())
}
false -> {
topLabel = "You Sent"
bottomLabel = "Total Sent"
bottomValue = "\$${tx?.value?.plus(ZcashSdk.MINERS_FEE_ZATOSHI).convertZatoshiToZecString()}"
topLabel = getString(R.string.transaction_story_outbound)
bottomLabel = getString(R.string.transaction_story_outbound_total)
bottomValue = "\$${WalletZecFormmatter.toZecStringFull(tx?.value?.plus(ZcashSdk.MINERS_FEE_ZATOSHI))}"
iconRotation = 135f
fee = "+0.0001 network fee"
source = "from your shielded wallet"
fee = getString(R.string.transaction_story_network_fee, WalletZecFormmatter.toZecStringFull(ZcashSdk.MINERS_FEE_ZATOSHI))
source = getString(R.string.transaction_story_from_shielded)
address = tx?.toAddress
}
null -> {

View File

@ -2,10 +2,14 @@ package cash.z.ecc.android.ui.history
import android.view.View
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R
import cash.z.ecc.android.ext.WalletZecFormmatter
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.locale
import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.ext.ZcashSdk
@ -16,7 +20,6 @@ import cash.z.ecc.android.ui.MainActivity
import cash.z.ecc.android.ui.util.toUtf8Memo
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val indicator = itemView.findViewById<View>(R.id.indicator)
@ -24,16 +27,16 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
private val topText = itemView.findViewById<TextView>(R.id.text_transaction_top)
private val bottomText = itemView.findViewById<TextView>(R.id.text_transaction_bottom)
private val shieldIcon = itemView.findViewById<View>(R.id.image_shield)
private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault())
private val formatter = SimpleDateFormat(itemView.context.getString(R.string.format_date_time), itemView.context.locale())
fun bindTo(transaction: T?) {
val mainActivity = itemView.context as MainActivity
mainActivity.lifecycleScope.launch {
// update view
var lineOne: String = ""
var lineTwo: String = ""
var amountZec: String = ""
var amountDisplay: String = ""
var lineOne = ""
var lineTwo = ""
var amountZec = ""
var amountDisplay = ""
var amountColor: Int = R.color.text_light_dimmed
var lineOneColor: Int = R.color.text_light
var lineTwoColor: Int = R.color.text_light_dimmed
@ -47,16 +50,16 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
onTransactionLongPressed(this)
true
}
amountZec = value.convertZatoshiToZecString()
amountZec = WalletZecFormmatter.toZecStringShort(value)
// TODO: these might be good extension functions
val timestamp = formatter.format(blockTimeInSeconds * 1000L)
val isMined = blockTimeInSeconds != 0L
when {
!toAddress.isNullOrEmpty() -> {
lineOne = "You paid ${toAddress?.toAbbreviatedAddress()}"
lineTwo = if (isMined) "Sent $timestamp" else "Pending confirmation"
lineOne = "${str(R.string.transaction_address_you_paid)} ${toAddress?.toAbbreviatedAddress()}"
lineTwo = if (isMined) "${str(R.string.transaction_status_sent)} $timestamp" else str(R.string.transaction_status_pending)
// TODO: this logic works but is sloppy. Find a more robust solution to displaying information about expiration (such as expires in 1 block, etc). Then if it is way beyond expired, remove it entirely. Perhaps give the user a button for that (swipe to dismiss?)
if(!isMined && (expiryHeight != null) && (expiryHeight!! < mainActivity.latestHeight ?: -1)) lineTwo = "Expired"
if(!isMined && (expiryHeight != null) && (expiryHeight!! < mainActivity.latestHeight ?: -1)) lineTwo = str(R.string.transaction_status_expired)
amountDisplay = "- $amountZec"
if (isMined) {
amountColor = R.color.zcashRed
@ -67,23 +70,23 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
}
}
toAddress.isNullOrEmpty() && value > 0L && minedHeight > 0 -> {
lineOne = "${mainActivity.getSender(transaction)} paid you"
lineTwo = "Received $timestamp"
lineOne = "${mainActivity.getSender(transaction)} ${str(R.string.transaction_address_paid_you)}"
lineTwo = "${str(R.string.transaction_received)} $timestamp"
amountDisplay = "+ $amountZec"
amountColor = R.color.zcashGreen
indicatorBackground = R.drawable.background_indicator_inbound
}
else -> {
lineOne = "Unknown"
lineTwo = "Unknown"
amountDisplay = "$amountZec"
lineOne = str(R.string.unknown)
lineTwo = str(R.string.unknown)
amountDisplay = amountZec
amountColor = R.color.text_light
}
}
// sanitize amount
if (value < ZcashSdk.MINERS_FEE_ZATOSHI) amountDisplay = "< 0.001"
else if (amountZec.length > 10) { // 10 allows 3 digits to the left and 6 to the right of the decimal
amountDisplay = "tap to view"
amountDisplay = str(R.string.transaction_instruction_tap)
}
}
@ -94,10 +97,10 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
topText.setTextColor(lineOneColor.toAppColor())
bottomText.setTextColor(lineTwoColor.toAppColor())
val context = itemView.context
indicator.background = context.resources.getDrawable(indicatorBackground)
indicator.background = AppCompatResources.getDrawable(itemView.context, indicatorBackground)
// TODO: change this so we see the shield if it is a z-addr in the address line but verify the intended design/behavior, first
shieldIcon.goneIf((transaction?.raw != null || transaction?.expiryHeight != null) && !transaction?.toAddress.isShielded())
shieldIcon.goneIf((transaction?.raw != null || transaction?.expiryHeight != null) && !transaction.toAddress.isShielded())
}
}
@ -114,6 +117,8 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
mainActivity.copyText(it, "Transaction Address")
}
}
private inline fun str(@StringRes resourceId: Int) = itemView.context.getString(resourceId)
}

View File

@ -5,6 +5,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentHomeBinding
@ -21,11 +22,9 @@ import cash.z.ecc.android.ui.send.SendViewModel
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.NO_SEED
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.scanReduce
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@ -65,7 +64,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
mainActivity?.safeNavigate(R.id.action_nav_home_to_create_wallet)
} else {
twig("Previous wallet found. Re-opening it.")
mainActivity?.startSync(walletSetup.buildInitializer())
mainActivity?.startSync(walletSetup.openStoredWallet())
}
}
}
@ -111,7 +110,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
if (::uiModel.isInitialized) {
twig("uiModel exists! it has pendingSend=${uiModel.pendingSend} ZEC while the sendViewModel=${sendViewModel.zatoshiAmount} zats")
// if the model already existed, cool but let the sendViewModel be the source of truth for the amount
onModelUpdated(null, uiModel.copy(pendingSend = sendViewModel.zatoshiAmount.coerceAtLeast(0).convertZatoshiToZecStringUniform(8)))
onModelUpdated(null, uiModel.copy(pendingSend = WalletZecFormmatter.toZecStringFull(sendViewModel.zatoshiAmount.coerceAtLeast(0))))
}
}
@ -133,9 +132,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
super.onResume()
twig("HomeFragment.onResume resumeScope.isActive: ${resumedScope.isActive} $resumedScope")
val existingAmount = sendViewModel.zatoshiAmount.coerceAtLeast(0)
viewModel.initializeMaybe(existingAmount.convertZatoshiToZecStringUniform(8))
viewModel.initializeMaybe(WalletZecFormmatter.toZecStringFull(existingAmount))
if (existingAmount == 0L) onClearAmount()
viewModel.uiModels.scanReduce { old, new ->
viewModel.uiModels.runningReduce { old, new ->
onModelUpdated(old, new)
new
}.onCompletion {
@ -157,9 +156,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
twig("HomeFragment.onSaveInstanceState")
if (::uiModel.isInitialized) {
// if (::uiModel.isInitialized) {
// outState.putParcelable("uiModel", uiModel)
}
// }
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
@ -204,20 +203,20 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
}
val sendText = when {
uiModel.status == DISCONNECTED -> "Reconnecting . . ."
uiModel.isSynced -> if (uiModel.hasFunds) "SEND AMOUNT" else "NO FUNDS AVAILABLE"
uiModel.status == STOPPED -> "IDLE"
uiModel.isDownloading -> "Downloading . . . ${snake.downloadProgress}%"
uiModel.isValidating -> "Validating . . ."
uiModel.isScanning -> "Scanning . . . ${snake.scanProgress}%"
else -> "Updating"
uiModel.status == DISCONNECTED -> getString(R.string.home_button_send_disconnected)
uiModel.isSynced -> if (uiModel.hasFunds) getString(R.string.home_button_send_has_funds) else getString(R.string.home_button_send_no_funds)
uiModel.status == STOPPED -> getString(R.string.home_button_send_idle)
uiModel.isDownloading -> getString(R.string.home_button_send_downloading, snake.downloadProgress)
uiModel.isValidating -> getString(R.string.home_button_send_validating)
uiModel.isScanning -> getString(R.string.home_button_send_scanning, snake.scanProgress)
else -> getString(R.string.home_button_send_updating)
}
binding.buttonSendAmount.text = sendText
twig("Send button set to: $sendText")
val resId = if (uiModel.isSynced) R.color.selector_button_text_dark else R.color.selector_button_text_light
binding.buttonSendAmount.setTextColor(resources.getColorStateList(resId))
context?.let { binding.buttonSendAmount.setTextColor(AppCompatResources.getColorStateList(it, resId)) }
binding.lottieButtonLoading.invisibleIf(uiModel.isDisconnected)
}
@ -229,23 +228,24 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
binding.textSendAmount.text = "\$$amount".toColoredSpan(R.color.text_light_dimmed, "$")
if (updateModel) {
sendViewModel.zatoshiAmount = amount.safelyConvertToBigDecimal().convertZecToZatoshi()
twig("dBUG: updating model. converting: $amount\tresult: ${sendViewModel.zatoshiAmount}\tprint: ${WalletZecFormmatter.toZecStringFull(sendViewModel.zatoshiAmount)}")
}
binding.buttonSendAmount.disabledIf(amount == "0")
}
fun setAvailable(availableBalance: Long = -1L, totalBalance: Long = -1L) {
val missingBalance = availableBalance < 0
val availableString = if (missingBalance) "Updating" else availableBalance.convertZatoshiToZecString()
val availableString = if (missingBalance) getString(R.string.home_button_send_updating) else WalletZecFormmatter.toZecStringFull(availableBalance)
binding.textBalanceAvailable.text = availableString
binding.textBalanceAvailable.transparentIf(missingBalance)
binding.labelBalance.transparentIf(missingBalance)
binding.textBalanceDescription.apply {
goneIf(missingBalance)
text = if (availableBalance != -1L && (availableBalance < totalBalance)) {
val change = (totalBalance - availableBalance).convertZatoshiToZecString()
"(expecting +$change ZEC)".toColoredSpan(R.color.text_light, "+$change")
val change = WalletZecFormmatter.toZecStringFull(totalBalance - availableBalance)
"(${getString(R.string.home_banner_expecting)} +$change ZEC)".toColoredSpan(R.color.text_light, "+$change")
} else {
"(enter an amount to send)"
getString(R.string.home_instruction_enter_amount)
}
}
}
@ -332,10 +332,10 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
when (action) {
FUND_NOW -> {
MaterialAlertDialogBuilder(activity)
.setMessage("To make full use of this wallet, deposit funds to your address.")
.setTitle("No Balance")
.setMessage(R.string.home_dialog_no_balance_message)
.setTitle(R.string.home_dialog_no_balance_title)
.setCancelable(true)
.setPositiveButton("View Address") { dialog, _ ->
.setPositiveButton(R.string.home_dialog_no_balance_button_positive) { dialog, _ ->
tapped(HOME_FUND_NOW)
dialog.dismiss()
mainActivity?.safeNavigate(R.id.action_nav_home_to_nav_receive)
@ -363,7 +363,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
}
private fun onNoFunds() {
setBanner("No Balance", FUND_NOW)
setBanner(getString(R.string.home_no_balance), FUND_NOW)
}

View File

@ -1,6 +1,8 @@
package cash.z.ecc.android.ui.home
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.R
import cash.z.ecc.android.ext.toAppString
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.Synchronizer.Status.*
@ -37,25 +39,26 @@ class HomeViewModel @Inject constructor() : ViewModel() {
}
_typedChars = ConflatedBroadcastChannel()
val typedChars = _typedChars.asFlow()
val decimal = '.'// R.string.key_decimal.toAppString()[0]
val backspace = R.string.key_backspace.toAppString()[0]
val zec = typedChars.scan(preTypedChars) { acc, c ->
when {
// no-op cases
acc == "0" && c == '0'
|| (c == '<' && acc == "0")
|| (c == '.' && acc.contains('.')) -> {twig("triggered: 1 acc: $acc c: $c")
|| (c == backspace && acc == "0")
|| (c == decimal && acc.contains(decimal)) -> {twig("triggered: 1 acc: $acc c: $c")
acc
}
c == '<' && acc.length <= 1 -> {twig("triggered: 2 $typedChars")
c == backspace && acc.length <= 1 -> {twig("triggered: 2 $typedChars")
"0"
}
c == '<' -> {twig("triggered: 3")
c == backspace -> {twig("triggered: 3")
acc.substring(0, acc.length - 1)
}
acc == "0" && c != '.' -> {twig("triggered: 4 $typedChars")
acc == "0" && c != decimal -> {twig("triggered: 4 $typedChars")
c.toString()
}
acc.contains('.') && acc.length - acc.indexOf('.') > 8 -> {
acc.contains(decimal) && acc.length - acc.indexOf(decimal) > 8 -> {
twig("triggered: 5 $typedChars")
acc
}

View File

@ -7,6 +7,7 @@ import android.view.ViewTreeObserver
import android.view.WindowManager
import android.widget.Toast
import androidx.core.view.doOnLayout
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentFeedbackBinding
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.feedback.Report
@ -67,7 +68,7 @@ class FeedbackFragment : BaseFragment<FragmentFeedbackBinding>() {
//
private fun onFeedbackSubmit(view: View) {
Toast.makeText(mainActivity, "Thanks for the feedback!", Toast.LENGTH_LONG).show()
Toast.makeText(mainActivity, R.string.feedback_thanks, Toast.LENGTH_LONG).show()
tapped(FEEDBACK_SUBMIT)
val q1 = binding.inputQuestion1.editText?.text.toString()

View File

@ -11,6 +11,7 @@ import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentProfileBinding
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.find
import cash.z.ecc.android.ext.onClick
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.ext.onClickNavTo
@ -18,14 +19,12 @@ import cash.z.ecc.android.feedback.FeedbackFile
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Funnel.UserFeedback
import cash.z.ecc.android.feedback.Report.Tap.*
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.sdk.ext.Bush
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.util.DebugFileTwig
import kotlinx.coroutines.launch
import okio.Okio
import java.io.File
import java.io.IOException
import java.lang.IllegalArgumentException
class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
@ -70,7 +69,13 @@ class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
}
private fun onViewDevLogs() {
shareFile(writeLogcat())
developerLogFile().let {
if (it == null) {
mainActivity?.showSnackbar("Error: No developer log found!")
} else {
shareFile(it)
}
}
}
private fun shareFiles(vararg files: File?) {
@ -85,7 +90,7 @@ class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
type = "text/*"
}
startActivity(Intent.createChooser(intent, "Share Log Files"))
startActivity(Intent.createChooser(intent, getString(R.string.profile_share_log_title)))
}
fun shareFile(file: File?) {
@ -95,36 +100,14 @@ class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
putExtra(Intent.EXTRA_STREAM, uri)
type = "text/plain"
}
startActivity(Intent.createChooser(intent, "Share Log File"))
startActivity(Intent.createChooser(intent, getString(R.string.profile_share_log_title)))
}
private fun userLogFile(): File? {
return mainActivity?.feedbackCoordinator?.findObserver<FeedbackFile>()?.file
}
private fun loadLogFileAsText(): String? {
val feedbackFile: File = userLogFile() ?: return null
Okio.buffer(Okio.source(feedbackFile)).use {
return it.readUtf8()
}
}
private fun writeLogcat(): File? {
try {
// Note: the /logs directory has been configured as a file provider under @xml/file_paths which allows the temporary sharing of this file
val outputFile = File("${ZcashWalletApp.instance.filesDir}/logs", "developer_log.txt").also { it.parentFile.mkdirs() }
if (!outputFile.parentFile.isDirectory) {
// addresses security finding in issue #121
throw IllegalArgumentException("Invalid path: ${outputFile.parentFile}. Verify" +
" that the default files directory is not being manipulated.")
}
val cmd = arrayOf("/bin/sh", "-c", "logcat -v time -d | grep '@TWIG' > '${outputFile.absolutePath}'")
Runtime.getRuntime().exec(cmd)
return outputFile
} catch (e: IOException) {
e.printStackTrace()
twig("Failed to create log")
}
return null
private fun developerLogFile(): File? {
return Bush.trunk.find<DebugFileTwig>()?.file
}
}

View File

@ -14,7 +14,6 @@ import cash.z.ecc.android.databinding.FragmentScanBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.ext.onClickNavTo
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.SCAN_BACK
import cash.z.ecc.android.sdk.ext.ZcashSdk
@ -47,8 +46,8 @@ class ScanFragment : BaseFragment<FragmentScanBinding>() {
binding.backButtonHitArea.onClickNavBack() { tapped(SCAN_BACK) }
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!allPermissionsGranted()) getRuntimePermissions()
}
@ -125,7 +124,7 @@ class ScanFragment : BaseFragment<FragmentScanBinding>() {
resumedScope.launch {
if (viewModel.isNotValid(qrContent)) {
val network = ZcashSdk.NETWORK
binding.textScanError.text = "Invalid Zcash $network address:\n$qrContent"
binding.textScanError.text = getString(R.string.scan_invalid_address, network, qrContent)
image.close()
} else { /* continue scanning*/
binding.textScanError.text = ""

View File

@ -52,7 +52,7 @@ class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
// Apply View Model
if (sendViewModel.zatoshiAmount > 0L) {
sendViewModel.zatoshiAmount.convertZatoshiToZecStringUniform(8).let { amount ->
WalletZecFormmatter.toZecStringFull(sendViewModel.zatoshiAmount).let { amount ->
binding.inputZcashAmount.setText(amount)
}
} else {
@ -123,7 +123,7 @@ class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
private fun onMax() {
if (maxZatoshi != null) {
binding.inputZcashAmount.apply {
setText(maxZatoshi.convertZatoshiToZecString(8))
setText(WalletZecFormmatter.toZecStringFull(maxZatoshi ?: 0L))
postDelayed({
requestFocus()
setSelection(text?.length ?: 0)
@ -156,7 +156,7 @@ class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
private fun onBalanceUpdated(balance: WalletBalance) {
binding.textLayoutAmount.helperText =
"You have ${balance.availableZatoshi.coerceAtLeast(0L).convertZatoshiToZecString(8)} available"
"You have ${WalletZecFormmatter.toZecStringFull(balance.availableZatoshi.coerceAtLeast(0L))} available"
maxZatoshi = (balance.availableZatoshi - ZcashSdk.MINERS_FEE_ZATOSHI).coerceAtLeast(0L)
}

View File

@ -7,6 +7,7 @@ import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendConfirmBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.WalletZecFormmatter
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onClickNavTo
import cash.z.ecc.android.feedback.Report
@ -36,7 +37,7 @@ class SendConfirmFragment : BaseFragment<FragmentSendConfirmBinding>() {
// }
mainActivity?.lifecycleScope?.launch {
binding.textConfirmation.text =
"Send ${sendViewModel.zatoshiAmount.convertZatoshiToZecString(8)} ZEC to ${sendViewModel?.toAddress.toAbbreviatedAddress()}?"
"Send ${WalletZecFormmatter.toZecStringFull(sendViewModel.zatoshiAmount)} ZEC to ${sendViewModel?.toAddress.toAbbreviatedAddress()}?"
}
sendViewModel.memo.trim().isNotEmpty().let { hasMemo ->
binding.radioIncludeAddress.isChecked = hasMemo || sendViewModel.includeFromAddress

View File

@ -1,5 +1,6 @@
package cash.z.ecc.android.ui.send
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
@ -8,6 +9,7 @@ import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendFinalBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.WalletZecFormmatter
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.SEND_FINAL_CLOSE
@ -23,11 +25,12 @@ import kotlinx.coroutines.flow.onEach
class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
override val screen = Report.Screen.SEND_FINAL
val sendViewModel: SendViewModel by activityViewModel()
private val sendViewModel: SendViewModel by activityViewModel()
override fun inflate(inflater: LayoutInflater): FragmentSendFinalBinding =
FragmentSendFinalBinding.inflate(inflater)
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonPrimary.setOnClickListener {
@ -40,7 +43,7 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
onExit().also { tapped(SEND_FINAL_CLOSE) }
}
binding.textConfirmation.text =
"Sending ${sendViewModel.zatoshiAmount.convertZatoshiToZecString(8)} ZEC to\n${sendViewModel.toAddress.toAbbreviatedAddress()}"
"${getString(R.string.send_final_sending)} ${WalletZecFormmatter.toZecStringFull(sendViewModel.zatoshiAmount)} ZEC\n${getString(R.string.send_final_to)}\n${sendViewModel.toAddress.toAbbreviatedAddress()}"
mainActivity?.preventBackPress(this)
}
@ -88,7 +91,7 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
private fun onExit() {
sendViewModel.reset()
mainActivity?.navController?.popBackStack(R.id.nav_home, false)
mainActivity?.safeNavigate(R.id.action_nav_send_final_to_nav_home)
}
private fun onCancel(tx: PendingTransaction) {
@ -96,7 +99,7 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
}
private fun onReturnToSend() {
mainActivity?.navController?.popBackStack(R.id.nav_send, false)
mainActivity?.safeNavigate(R.id.action_nav_send_final_to_nav_send)
}
private fun onSeeDetails() {
@ -107,30 +110,31 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
private fun PendingTransaction.toUiModel() = UiModel().also { model ->
when {
isCancelled() -> {
model.title = "Cancelled."
model.primaryButtonText = "Go Back"
model.title = getString(R.string.send_final_result_cancelled)
model.primaryButtonText = getString(R.string.send_final_button_primary_back)
model.primaryAction = { onReturnToSend() }
}
isSubmitSuccess() -> {
model.title = "SENT!"
model.primaryButtonText = "See Details"
model.title = getString(R.string.send_final_button_primary_sent)
model.primaryButtonText = getString(R.string.send_final_button_primary_details)
model.primaryAction = { onSeeDetails() }
}
isFailure() -> {
model.title = "Failed."
model.errorMessage = if (isFailedEncoding()) "The transaction could not be encoded." else "Unable to submit transaction to the network."
model.primaryButtonText = "Retry"
model.title = getString(R.string.send_final_button_primary_failed)
model.errorMessage = if (isFailedEncoding()) getString(R.string.send_final_error_encoding) else getString(
R.string.send_final_error_submitting)
model.primaryButtonText = getString(R.string.send_final_button_primary_retry)
model.primaryAction = { onReturnToSend() }
}
else -> {
model.title = "Sending ${value.convertZatoshiToZecString(8)} ZEC to\n${toAddress.toAbbreviatedAddress()}"
model.title = "${getString(R.string.send_final_sending)} ${WalletZecFormmatter.toZecStringFull(value)} ZEC ${getString(R.string.send_final_to)}\n${toAddress.toAbbreviatedAddress()}"
model.showProgress = true
if (isCreating()) {
model.showCloseIcon = false
model.primaryButtonText = "Cancel"
model.primaryButtonText = getString(R.string.send_final_button_primary_cancel)
model.primaryAction = { onCancel(this) }
} else {
model.primaryButtonText = "See Details"
model.primaryButtonText = getString(R.string.send_final_button_primary_details)
model.primaryAction = { onSeeDetails() }
}
}

View File

@ -14,6 +14,7 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendBinding
@ -105,7 +106,7 @@ class SendFragment : BaseFragment<FragmentSendBinding>(),
private fun applyViewModel(model: SendViewModel) {
// apply amount
val roundedAmount =
model.zatoshiAmount.coerceAtLeast(0L).convertZatoshiToZecStringUniform(8)
WalletZecFormmatter.toZecStringFull(model.zatoshiAmount.coerceAtLeast(0L))
binding.textSendAmount.text = "\$$roundedAmount"
// apply address
binding.inputZcashAddress.setText(model.toAddress)
@ -117,7 +118,7 @@ class SendFragment : BaseFragment<FragmentSendBinding>(),
private fun onMemoUpdated() {
val totalLength = sendViewModel.createMemoToSend().length
binding.textLayoutMemo.helperText = "$totalLength/${ZcashSdk.MAX_MEMO_SIZE} chars"
binding.textLayoutMemo.helperText = "$totalLength/${ZcashSdk.MAX_MEMO_SIZE} ${getString(R.string.send_memo_chars_abbreviation)}"
val color = if (totalLength > ZcashSdk.MAX_MEMO_SIZE) R.color.zcashRed else R.color.text_light_dimmed
binding.textLayoutMemo.setHelperTextColor(ColorStateList.valueOf(color.toAppColor()))
}
@ -135,17 +136,17 @@ class SendFragment : BaseFragment<FragmentSendBinding>(),
}
private fun onAddressChanged(address: String) {
resumedScope.launch {
lifecycleScope.launchWhenResumed {
val validation = sendViewModel.validateAddress(address)
binding.buttonSend.isActivated = !validation.isNotValid
var type = when (validation) {
is AddressType.Transparent -> "This is a valid transparent address" to R.color.zcashGreen
is AddressType.Shielded -> "This is a valid shielded address" to R.color.zcashGreen
is AddressType.Invalid -> "This address appears to be invalid" to R.color.zcashRed
is AddressType.Transparent -> R.string.send_validation_address_valid_taddr to R.color.zcashGreen
is AddressType.Shielded -> R.string.send_validation_address_valid_zaddr to R.color.zcashGreen
is AddressType.Invalid -> R.string.send_validation_address_invalid to R.color.zcashRed
}
if (address == sendViewModel.synchronizer.getAddress()) type =
"Warning, this appears to be your address!" to R.color.zcashRed
binding.textLayoutAddress.helperText = type.first
R.string.send_validation_address_self to R.color.zcashRed
binding.textLayoutAddress.helperText = getString(type.first)
binding.textLayoutAddress.setHelperTextColor(ColorStateList.valueOf(type.second.toAppColor()))
// if we have the clipboard address but we're changing it, then clear the selection
@ -170,9 +171,9 @@ class SendFragment : BaseFragment<FragmentSendBinding>(),
private fun onSubmit(unused: EditText? = null) {
sendViewModel.toAddress = binding.inputZcashAddress.text.toString()
sendViewModel.validate(availableZatoshi, maxZatoshi).onFirstWith(resumedScope) { errorMessage ->
sendViewModel.validate(requireContext(), availableZatoshi, maxZatoshi).onFirstWith(resumedScope) { errorMessage ->
if (errorMessage == null) {
mainActivity?.authenticate("Please confirm that you want to send\n${sendViewModel.zatoshiAmount.convertZatoshiToZecString(8)} ZEC to\n${sendViewModel.toAddress.toAbbreviatedAddress()}") {
mainActivity?.authenticate("${getString(R.string.send_confirmation_prompt)}\n${WalletZecFormmatter.toZecStringFull(sendViewModel.zatoshiAmount)} ZEC ${getString(R.string.send_final_to)}\n${sendViewModel.toAddress.toAbbreviatedAddress()}") {
// sendViewModel.funnel(Send.AddressPageComplete)
mainActivity?.safeNavigate(R.id.action_nav_send_to_nav_send_final)
}
@ -189,7 +190,7 @@ class SendFragment : BaseFragment<FragmentSendBinding>(),
private fun onMax() {
if (maxZatoshi != null) {
// binding.inputZcashAmount.apply {
// setText(maxZatoshi.convertZatoshiToZecString(8))
// setText(WalletZecFormmatter.toZecStringFull(maxZatoshi))
// postDelayed({
// requestFocus()
// setSelection(text?.length ?: 0)
@ -223,7 +224,7 @@ class SendFragment : BaseFragment<FragmentSendBinding>(),
private fun onBalanceUpdated(balance: WalletBalance) {
// binding.textLayoutAmount.helperText =
// "You have ${balance.availableZatoshi.coerceAtLeast(0L).convertZatoshiToZecString(8)} available"
// "You have ${WalletZecFormmatter.toZecStringFull(balance.availableZatoshi.coerceAtLeast(0L))} available"
maxZatoshi = (balance.availableZatoshi - ZcashSdk.MINERS_FEE_ZATOSHI).coerceAtLeast(0L)
availableZatoshi = balance.availableZatoshi
}
@ -264,7 +265,7 @@ class SendFragment : BaseFragment<FragmentSendBinding>(),
selected,
address.takeUnless { isBoth })
}
binding.dividerClipboard.text = if (isBoth) "Last Used and On Clipboard" else "On Clipboard"
binding.dividerClipboard.setText(if (isBoth) R.string.send_history_last_and_clipboard else R.string.send_history_clipboard)
}
private fun updateAddressBanner(

View File

@ -1,7 +1,11 @@
package cash.z.ecc.android.ui.send
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.R
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.ext.WalletZecFormmatter
import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.feedback.Feedback.Keyed
import cash.z.ecc.android.feedback.Feedback.TimeMetric
@ -13,6 +17,7 @@ import cash.z.ecc.android.feedback.Report.MetricType
import cash.z.ecc.android.feedback.Report.MetricType.*
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.annotation.OpenForTesting
import cash.z.ecc.android.sdk.db.entity.*
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
@ -31,7 +36,8 @@ import javax.inject.Inject
class SendViewModel @Inject constructor() : ViewModel() {
private val metrics = mutableMapOf<String, TimeMetric>()
// note used in testing
val metrics = mutableMapOf<String, TimeMetric>()
@Inject
lateinit var lockBox: LockBox
@ -60,7 +66,7 @@ class SendViewModel @Inject constructor() : ViewModel() {
funnel(SendSelected)
val memoToSend = createMemoToSend()
val keys = DerivationTool.deriveSpendingKeys(
lockBox.getBytes(WalletSetupViewModel.LockBoxKey.SEED)!!
lockBox.getBytes(Const.Backup.SEED)!!
)
funnel(SpendingKeyFound)
reportIssues(memoToSend)
@ -100,29 +106,29 @@ class SendViewModel @Inject constructor() : ViewModel() {
suspend fun validateAddress(address: String): AddressType =
synchronizer.validateAddress(address)
fun validate(availableZatoshi: Long?, maxZatoshi: Long?) = flow<String?> {
fun validate(context: Context, availableZatoshi: Long?, maxZatoshi: Long?) = flow<String?> {
when {
synchronizer.validateAddress(toAddress).isNotValid -> {
emit("Please enter a valid address.")
emit(context.getString(R.string.send_validation_error_address_invalid))
}
zatoshiAmount < 1 -> {
emit("Please go back and enter at least 1 Zatoshi.")
emit(context.getString(R.string.send_validation_error_amount_minimum))
}
availableZatoshi == null -> {
emit("Available funds not found. Please try again in a moment.")
emit(context.getString(R.string.send_validation_error_unknown_funds))
}
availableZatoshi == 0L -> {
emit("No funds available to send.")
emit(context.getString(R.string.send_validation_error_no_available_funds))
}
availableZatoshi > 0 && availableZatoshi < ZcashSdk.MINERS_FEE_ZATOSHI -> {
emit("Insufficient funds to cover miner's fee.")
emit(context.getString(R.string.send_validation_error_dust))
}
maxZatoshi != null && zatoshiAmount > maxZatoshi -> {
emit( "Please go back and enter no more than ${maxZatoshi.convertZatoshiToZecString(8)} ZEC.")
emit(context.getString(R.string.send_validation_error_too_much, WalletZecFormmatter.toZecStringFull(maxZatoshi)))
}
createMemoToSend().length > ZcashSdk.MAX_MEMO_SIZE -> {
emit( "Memo must be less than ${ZcashSdk.MAX_MEMO_SIZE} in length.")
emit(context.getString(R.string.send_validation_error_memo_length, ZcashSdk.MAX_MEMO_SIZE))
}
else -> emit(null)
}

View File

@ -60,7 +60,7 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding>() {
private fun onResetClicked(unused: View?) {
mainActivity?.hideKeyboard()
context?.showUpdateServerDialog("Restore Defaults") {
context?.showUpdateServerDialog(R.string.settings_buttons_restore) {
resumedScope.launch {
binding.groupLoading.visible()
binding.loadingView.requestFocus()
@ -117,7 +117,7 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding>() {
if (uiModel.complete) {
binding.groupLoading.gone()
mainActivity?.safeNavigate(R.id.nav_home)
Toast.makeText(ZcashWalletApp.instance, "Successfully changed server!", Toast.LENGTH_SHORT).show()
Toast.makeText(ZcashWalletApp.instance, getString(R.string.settings_toast_change_server_success), Toast.LENGTH_SHORT).show()
true
}
false
@ -133,7 +133,7 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding>() {
val message = "An error occured while changing servers. Please verify the info" +
" and try again.\n\nError: $details"
twig(message)
Toast.makeText(ZcashWalletApp.instance, "Failed to change server!", Toast.LENGTH_SHORT).show()
Toast.makeText(ZcashWalletApp.instance, getString(R.string.settings_toast_change_server_failure), Toast.LENGTH_SHORT).show()
context?.showUpdateServerCriticalError(message)
}

View File

@ -10,9 +10,11 @@ import android.widget.TextView
import android.widget.Toast
import androidx.activity.addCallback
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentBackupBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.MetricType.SEED_PHRASE_LOADED
import cash.z.ecc.android.feedback.Report.Tap.BACKUP_DONE
@ -22,7 +24,6 @@ import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITH_BACKUP
import cash.z.ecc.android.ui.util.AddressPartNumberSpan
import cash.z.ecc.kotlin.mnemonic.Mnemonics
@ -36,7 +37,7 @@ import kotlinx.coroutines.withContext
class BackupFragment : BaseFragment<FragmentBackupBinding>() {
override val screen = Report.Screen.BACKUP
val walletSetup: WalletSetupViewModel by activityViewModel(false)
private val walletSetup: WalletSetupViewModel by activityViewModel(false)
private var hasBackUp: Boolean = true //TODO: implement backup and then check for it here-ish
@ -61,7 +62,7 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
onEnterWallet().also { if (hasBackUp) tapped(BACKUP_DONE) else tapped(BACKUP_VERIFY) }
}
if (hasBackUp) {
binding.buttonPositive.text = "Done"
binding.buttonPositive.text = getString(R.string.backup_button_done)
}
}
@ -74,10 +75,9 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
override fun onAttach(context: Context) {
super.onAttach(context)
walletSetup.checkSeed().onEach {
when(it) {
SEED_WITH_BACKUP -> {
hasBackUp = true
}
hasBackUp = when(it) {
SEED_WITH_BACKUP -> true
else -> false
}
}.launchIn(lifecycleScope)
}
@ -85,14 +85,14 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
override fun onResume() {
super.onResume()
resumedScope.launch {
binding.textBirtdate.text = "Birthday Height: %,d".format(calculateBirthday())
binding.textBirtdate.text = getString(R.string.backup_format_birthday_height, calculateBirthday())
}
}
// TODO: move this into the SDK
private suspend fun calculateBirthday(): Int {
var storedBirthday: Int = 0
var oldestTransactionHeight:Int = 0
var storedBirthday = 0
var oldestTransactionHeight = 0
try {
storedBirthday = walletSetup.loadBirthdayHeight() ?: 0
oldestTransactionHeight = mainActivity?.synchronizerComponent?.synchronizer()?.receivedTransactions?.first()?.last()?.minedHeight ?: 0
@ -109,7 +109,7 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
private fun onEnterWallet(showMessage: Boolean = !this.hasBackUp) {
if (showMessage) {
Toast.makeText(activity, "Backup verification coming soon!", Toast.LENGTH_LONG).show()
Toast.makeText(activity, R.string.backup_verification_not_implemented, Toast.LENGTH_LONG).show()
}
mainActivity?.navController?.popBackStack()
}
@ -131,7 +131,7 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
mainActivity!!.feedback.measure(SEED_PHRASE_LOADED) {
val lockBox = LockBox(ZcashWalletApp.instance)
val mnemonics = Mnemonics()
val seedPhrase = lockBox.getCharsUtf8(LockBoxKey.SEED_PHRASE)!!
val seedPhrase = lockBox.getCharsUtf8(Const.Backup.SEED_PHRASE)!!
val result = mnemonics.toWordList(seedPhrase)
result
}

View File

@ -4,10 +4,13 @@ import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentLandingBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.locale
import cash.z.ecc.android.ext.toAppString
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Funnel.Restore
import cash.z.ecc.android.feedback.Report.Tap.*
@ -18,6 +21,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.lang.IllegalStateException
class LandingFragment : BaseFragment<FragmentLandingBinding>() {
override val screen = Report.Screen.LANDING
@ -32,14 +36,14 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonPositive.setOnClickListener {
when (binding.buttonPositive.text.toString().toLowerCase()) {
"new" -> onNewWallet().also { tapped(LANDING_NEW) }
"backup" -> onBackupWallet().also { tapped(LANDING_BACKUP) }
when (binding.buttonPositive.text.toString().toLowerCase(locale())) {
R.string.landing_button_primary.toAppString(true) -> onNewWallet().also { tapped(LANDING_NEW) }
R.string.landing_button_primary_create_success.toAppString(true) -> onBackupWallet().also { tapped(LANDING_BACKUP) }
}
}
binding.buttonNegative.setOnLongClickListener {
tapped(DEVELOPER_WALLET_PROMPT)
if (binding.buttonNegative.text.toString().toLowerCase() == "restore") {
if (binding.buttonNegative.text.toString().toLowerCase(locale()) == "restore") {
MaterialAlertDialogBuilder(activity)
.setMessage("Would you like to import the dev wallet?\n\nIf so, please only send 0.0001 ZEC at a time and return some later so that the account remains funded.")
.setTitle("Import Dev Wallet?")
@ -55,12 +59,13 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
}
.show()
true
} else {
false
}
false
}
binding.buttonNegative.setOnClickListener {
when (binding.buttonNegative.text.toString().toLowerCase()) {
"restore" -> onRestoreWallet().also {
when (binding.buttonNegative.text.toString().toLowerCase(locale())) {
R.string.landing_button_secondary.toAppString(true) -> onRestoreWallet().also {
mainActivity?.reportFunnel(Restore.Initiated)
tapped(LANDING_RESTORE)
}
@ -76,6 +81,7 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
SEED_WITHOUT_BACKUP, SEED_WITH_BACKUP -> {
mainActivity?.safeNavigate(R.id.nav_backup)
}
else -> {}
}
}.launchIn(lifecycleScope)
}
@ -89,15 +95,13 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
when (count) {
1 -> {
tapped(LANDING_BACKUP_SKIPPED_1)
binding.textMessage.text =
"Are you sure? Without a backup, funds can be lost FOREVER!"
binding.buttonNegative.text = "Later"
binding.textMessage.setText(R.string.landing_backup_skipped_message_1)
binding.buttonNegative.setText(R.string.landing_button_backup_skipped_1)
}
2 -> {
tapped(LANDING_BACKUP_SKIPPED_2)
binding.textMessage.text =
"You can't backup later. You're probably going to lose your funds!"
binding.buttonNegative.text = "I've been warned"
binding.textMessage.setText(R.string.landing_backup_skipped_message_2)
binding.buttonNegative.setText(R.string.landing_button_backup_skipped_2)
}
else -> {
tapped(LANDING_BACKUP_SKIPPED_3)
@ -113,15 +117,15 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
// AKA import wallet
private fun onUseDevWallet() {
val seedPhrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
val birthday = 663174//626599
val birthday = 663174 //991645
mainActivity?.apply {
lifecycleScope.launch {
mainActivity?.startSync(walletSetup.importWallet(seedPhrase, birthday))
}
binding.buttonPositive.isEnabled = true
binding.textMessage.text = "Wallet imported! Congratulations!"
binding.buttonNegative.text = "Skip"
binding.buttonPositive.text = "Backup"
binding.textMessage.setText(R.string.landing_import_success_message)
binding.buttonNegative.setText(R.string.landing_button_secondary_import_success)
binding.buttonPositive.setText(R.string.landing_import_success_primary_button)
playSound("sound_receive_small.mp3")
vibrateSuccess()
}
@ -129,18 +133,27 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
private fun onNewWallet() {
lifecycleScope.launch {
val ogText = binding.buttonPositive.text
binding.buttonPositive.text = "creating"
binding.buttonPositive.setText(R.string.landing_button_progress_create)
binding.buttonPositive.isEnabled = false
mainActivity?.startSync(walletSetup.newWallet())
try {
val initializer = walletSetup.newWallet()
if (!initializer.accountsCreated) {
binding.buttonPositive.isEnabled = true
binding.buttonPositive.setText(R.string.landing_button_primary)
throw IllegalStateException("New wallet should result in accounts table being created")
}
mainActivity?.startSync(initializer)
binding.buttonPositive.isEnabled = true
binding.textMessage.text = "Wallet created! Congratulations!"
binding.buttonNegative.text = "Skip"
binding.buttonPositive.text = "Backup"
mainActivity?.playSound("sound_receive_small.mp3")
mainActivity?.vibrateSuccess()
binding.buttonPositive.isEnabled = true
binding.textMessage.setText(R.string.landing_create_success_message)
binding.buttonNegative.setText(R.string.landing_button_secondary_create_success)
binding.buttonPositive.setText(R.string.landing_button_primary_create_success)
mainActivity?.playSound("sound_receive_small.mp3")
mainActivity?.vibrateSuccess()
} catch (t: Throwable) {
Toast.makeText(context, "Failed to create wallet", Toast.LENGTH_SHORT).show()
}
}
}

View File

@ -1,24 +1,20 @@
package cash.z.ecc.android.ui.setup
import android.util.Log
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.feedback.Report.MetricType.*
import cash.z.ecc.android.feedback.measure
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.exception.InitializerException
import cash.z.ecc.android.sdk.ext.toHex
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.*
import cash.z.ecc.kotlin.mnemonic.Mnemonics
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Named
@ -39,152 +35,17 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
lateinit var feedback: Feedback
enum class WalletSetupState {
UNKNOWN, SEED_WITH_BACKUP, SEED_WITHOUT_BACKUP, NO_SEED
SEED_WITH_BACKUP, SEED_WITHOUT_BACKUP, NO_SEED
}
fun checkSeed(): Flow<WalletSetupState> = flow {
when {
lockBox.getBoolean(LockBoxKey.HAS_BACKUP) -> emit(SEED_WITH_BACKUP)
lockBox.getBoolean(LockBoxKey.HAS_SEED) -> emit(SEED_WITHOUT_BACKUP)
lockBox.getBoolean(Const.Backup.HAS_BACKUP) -> emit(SEED_WITH_BACKUP)
lockBox.getBoolean(Const.Backup.HAS_SEED) -> emit(SEED_WITHOUT_BACKUP)
else -> emit(NO_SEED)
}
}
suspend fun buildInitializer() = ZcashWalletApp.component.initializerSubcomponent().create(loadConfig().single()).initializer()
private fun loadConfig() = flow<Initializer.Builder> {
emit(
Initializer.Builder { builder ->
val vk = lockBox.getCharsUtf8(LockBoxKey.VIEWING_KEY)?.let { String(it) }
?: throw InitializerException.MissingViewingKeyException
val birthdayHeight = loadBirthdayHeight()
?: throw InitializerException.MissingBirthdayException
val host = prefs[Const.Pref.SERVER_HOST] ?: Const.Default.Server.HOST
val port = prefs[Const.Pref.SERVER_PORT] ?: Const.Default.Server.PORT
builder.import(vk, birthdayHeight, host, port)
}
)
}
fun loadBirthdayHeight(): Int? {
val h: Int? = lockBox[LockBoxKey.BIRTHDAY_HEIGHT]
twigFix("Loaded birthday with key ${LockBoxKey.BIRTHDAY_HEIGHT} and found $h")
return h
}
// /**
// * Re-open an existing wallet. This is the most common use case, where a user has previously
// * created or imported their seed and is returning to the wallet. In other words, this is the
// * non-FTUE case.
// */
// fun openWallet(): Initializer {
// twigFix("Opening existing wallet")
// return ZcashWalletApp.component.initializerSubcomponent()
// .create(DefaultBirthdayStore(ZcashWalletApp.instance)).run {
// initializer().open(birthdayStore().getBirthday())
// }
// }
suspend fun newWallet(): Initializer {
twigFix("Initializing new wallet")
createWallet()
return buildInitializer()
}
suspend fun importWallet(seedPhrase: String, birthdayHeight: Int): Initializer {
twigFix("Importing wallet. Requested birthday: $birthdayHeight")
val seedPhraseChars = seedPhrase.toCharArray()
storeSeedPhrase(seedPhraseChars)
storeSeed(mnemonics.toSeed(seedPhraseChars))
WalletBirthdayTool.loadNearest(ZcashWalletApp.instance, birthdayHeight).let { birthday ->
storeBirthday(birthday)
}
return buildInitializer()
}
private fun twigFix(s: String) {
Log.e("@TWIG", s)
}
/**
* Take all the steps necessary to create a new wallet and measure how long it takes.
*
* @param feedback the object used for measurement.
*/
private suspend fun createWallet(): ByteArray = withContext(Dispatchers.IO) {
check(!lockBox.getBoolean(LockBoxKey.HAS_SEED)) {
"Error! Cannot create a seed when one already exists! This would overwrite the" +
" existing seed and could lead to a loss of funds if the user has no backup!"
}
feedback.measure(WALLET_CREATED) {
mnemonics.run {
feedback.measure(ENTROPY_CREATED) { nextEntropy() }.let { entropy ->
feedback.measure(SEED_PHRASE_CREATED) { nextMnemonic(entropy) }
.let { seedPhrase ->
feedback.measure(SEED_CREATED) { toSeed(seedPhrase) }.let { bip39Seed ->
storeSeedPhrase(seedPhrase)
storeSeed(bip39Seed)
WalletBirthdayTool.loadNearest(ZcashWalletApp.instance).let { birthday ->
storeBirthday(birthday)
}
bip39Seed
}
}
}
}
}
}
private fun storeBirthday(birthday: WalletBirthdayTool.WalletBirthday) {
twigFix("Storing birthday ${birthday.height} with and key ${LockBoxKey.BIRTHDAY_HEIGHT}")
lockBox[LockBoxKey.BIRTHDAY_HEIGHT] = birthday.height
}
private fun storeSeed(bip39Seed: ByteArray) {
twigFix("Storing seed: ${bip39Seed.toHex().length}")
lockBox.setBytes(LockBoxKey.SEED, bip39Seed)
lockBox[LockBoxKey.VIEWING_KEY] = DerivationTool.deriveViewingKeys(bip39Seed)[0]
lockBox[LockBoxKey.HAS_SEED] = true
}
private fun storeSeedPhrase(seedPhrase: CharArray) {
twigFix("Storing seedphrase: ${seedPhrase.size}")
lockBox[LockBoxKey.SEED_PHRASE] = seedPhrase
lockBox[LockBoxKey.HAS_SEED_PHRASE] = true
}
/**
* Take all the steps necessary to import a wallet and measure how long it takes.
*
* @param feedback the object used for measurement.
*/
private suspend fun importWallet(
seedPhrase: CharArray
): ByteArray = withContext(Dispatchers.IO) {
check(!lockBox.getBoolean(LockBoxKey.HAS_SEED)) {
"Error! Cannot import a seed when one already exists! This would overwrite the" +
" existing seed and could lead to a loss of funds if the user has no backup!"
}
feedback.measure(WALLET_IMPORTED) {
mnemonics.run {
feedback.measure(SEED_IMPORTED) { toSeed(seedPhrase) }.let { bip39Seed ->
storeSeedPhrase(seedPhrase)
lockBox.setBytes(LockBoxKey.SEED, bip39Seed)
lockBox[LockBoxKey.HAS_SEED] = true
bip39Seed
}
}
}
}
/**
* Throw an exception if the seed phrase is bad.
*/
@ -192,15 +53,94 @@ class WalletSetupViewModel @Inject constructor() : ViewModel() {
mnemonics.validate(seedPhrase.toCharArray())
}
object LockBoxKey {
const val SEED = "cash.z.ecc.android.SEED"
const val SEED_PHRASE = "cash.z.ecc.android.SEED_PHRASE"
const val HAS_SEED = "cash.z.ecc.android.HAS_SEED"
const val HAS_SEED_PHRASE = "cash.z.ecc.android.HAS_SEED_PHRASE"
const val HAS_BACKUP = "cash.z.ecc.android.HAS_BACKUP"
// Config
const val VIEWING_KEY = "cash.z.ecc.android.VIEWING_KEY"
const val BIRTHDAY_HEIGHT = "cash.z.ecc.android.BIRTHDAY_HEIGHT"
fun loadBirthdayHeight(): Int? {
val h: Int? = lockBox[Const.Backup.BIRTHDAY_HEIGHT]
twig("Loaded birthday with key ${Const.Backup.BIRTHDAY_HEIGHT} and found $h")
return h
}
suspend fun newWallet(): Initializer {
twig("Initializing new wallet")
with(mnemonics) {
storeWallet(nextMnemonic(nextEntropy()), loadNearestBirthday())
}
return openStoredWallet()
}
suspend fun importWallet(seedPhrase: String, birthdayHeight: Int): Initializer {
twig("Importing wallet. Requested birthday: $birthdayHeight")
storeWallet(seedPhrase.toCharArray(), loadNearestBirthday(birthdayHeight))
return openStoredWallet()
}
suspend fun openStoredWallet(): Initializer {
val config = loadConfig()
return ZcashWalletApp.component.initializerSubcomponent().create(config).initializer()
}
private fun loadConfig() = Initializer.Builder { builder ->
val vk = lockBox.getCharsUtf8(Const.Backup.VIEWING_KEY)?.let { String(it) }
?: throw InitializerException.MissingViewingKeyException
val birthdayHeight = loadBirthdayHeight()
?: throw InitializerException.MissingBirthdayException
val host = prefs[Const.Pref.SERVER_HOST] ?: Const.Default.Server.HOST
val port = prefs[Const.Pref.SERVER_PORT] ?: Const.Default.Server.PORT
builder.import(vk, birthdayHeight, host, port)
}
private fun loadNearestBirthday(birthdayHeight: Int? = null) =
WalletBirthdayTool.loadNearest(ZcashWalletApp.instance, birthdayHeight)
//
// Storage Helpers
//
/**
* Entry point for all storage. Takes a seed phrase and stores all the parts so that we can
* selectively use them, the next time the app is opened. Although we store everything, we
* primarily only work with the viewing key and spending key. The seed is only accessed when
* presenting backup information to the user.
*/
private suspend fun storeWallet(seedPhraseChars: CharArray, birthday: WalletBirthdayTool.WalletBirthday) {
check(!lockBox.getBoolean(Const.Backup.HAS_SEED)) {
"Error! Cannot store a seed when one already exists! This would overwrite the" +
" existing seed and could lead to a loss of funds if the user has no backup!"
}
storeBirthday(birthday)
mnemonics.toSeed(seedPhraseChars).let { bip39Seed ->
DerivationTool.deriveViewingKeys(bip39Seed)[0].let { viewingKey ->
storeSeedPhrase(seedPhraseChars)
storeSeed(bip39Seed)
storeViewingKey(viewingKey)
}
}
}
private suspend fun storeBirthday(birthday: WalletBirthdayTool.WalletBirthday) = withContext(IO) {
twig("Storing birthday ${birthday.height} with and key ${Const.Backup.BIRTHDAY_HEIGHT}")
lockBox[Const.Backup.BIRTHDAY_HEIGHT] = birthday.height
}
private suspend fun storeSeedPhrase(seedPhrase: CharArray) = withContext(IO) {
twig("Storing seedphrase: ${seedPhrase.size}")
lockBox[Const.Backup.SEED_PHRASE] = seedPhrase
lockBox[Const.Backup.HAS_SEED_PHRASE] = true
}
private suspend fun storeSeed(bip39Seed: ByteArray) = withContext(IO) {
twig("Storing seed: ${bip39Seed.size}")
lockBox.setBytes(Const.Backup.SEED, bip39Seed)
lockBox[Const.Backup.HAS_SEED] = true
}
private suspend fun storeViewingKey(vk: String) = withContext(IO) {
twig("storeViewingKey vk: ${vk.length}")
lockBox[Const.Backup.VIEWING_KEY] = vk
}
}

View File

@ -0,0 +1,21 @@
package cash.z.ecc.android.ui.util
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.sdk.ext.TroubleshootingTwig
import okio.Okio
import java.io.File
class DebugFileTwig(fileName: String = "developer_log.txt") : TroubleshootingTwig() {
val file = File("${ZcashWalletApp.instance.filesDir}/logs", fileName)
override fun twig(logMessage: String) {
super.twig(logMessage)
appendToFile(formatter(logMessage))
}
private fun appendToFile(message: String) {
Okio.buffer(Okio.appendingSink(file)).use {
it.writeUtf8("$message\n")
}
}
}

View File

@ -15,8 +15,8 @@
android:centerX="54"
android:centerY="36.01165"
android:type="radial">
<item android:offset="0" android:color="#FF3F3F4F"/>
<item android:offset="1" android:color="#FF000000"/>
<item android:offset="0" android:color="@color/app_icon_background_0"/>
<item android:offset="1" android:color="@color/app_icon_background_1"/>
</gradient>
</aapt:attr>
</path>

View File

@ -45,7 +45,7 @@
<path
android:pathData="M77.8,53.601C77.8,53.79 77.799,53.963 77.799,53.963L46.564,53.967C46.755,57.942 50.012,61.117 53.999,61.117C56.367,61.117 58.475,59.995 59.842,58.255L60.739,58.255C59.264,60.419 56.794,61.843 53.999,61.843C49.752,61.843 46.262,58.557 45.872,54.375L45.862,54.261C45.855,54.164 45.855,54.065 45.851,53.966L39.9,53.967C39.769,54.519 39.351,54.953 38.814,55.109C39.569,62.892 46.087,68.996 54,68.996C60.81,68.996 66.592,64.475 68.553,58.254L69.305,58.254C67.32,64.881 61.211,69.721 54,69.721C45.704,69.721 38.873,63.313 38.097,55.149C37.49,55.035 37.009,54.569 36.866,53.967L32.082,53.968C31.949,54.527 31.522,54.965 30.974,55.116C31.752,67.246 41.779,76.875 54,76.875C65.147,76.875 74.471,68.865 76.615,58.254L77.346,58.254C75.19,69.267 65.545,77.6 54,77.6C41.392,77.6 31.048,67.661 30.254,55.146C29.538,55.001 29,54.364 29,53.6C29,52.836 29.539,52.199 30.254,52.054C31.048,39.539 41.391,29.6 54,29.6C67.003,29.601 77.597,40.172 77.791,53.239C77.793,53.36 77.8,53.48 77.8,53.601ZM77.072,53.239C76.878,40.572 66.606,30.327 53.999,30.327C41.778,30.327 31.75,39.957 30.973,52.085C31.522,52.236 31.95,52.677 32.082,53.239L36.863,53.239C37.005,52.635 37.487,52.167 38.095,52.053C38.871,43.888 45.702,37.48 53.999,37.48C61.069,37.48 67.078,42.134 69.182,48.561L68.422,48.561C66.347,42.541 60.667,38.206 53.999,38.206C46.086,38.206 39.568,44.31 38.813,52.092C39.351,52.25 39.77,52.685 39.9,53.239L45.845,53.239C45.852,53.091 45.86,52.961 45.86,52.961C46.186,48.716 49.706,45.36 53.999,45.36C56.623,45.36 58.955,46.617 60.451,48.561L59.513,48.561C58.148,47.045 56.185,46.085 53.999,46.085C50.01,46.085 46.752,49.263 46.562,53.239L46.562,53.239L77.072,53.239Z"
android:strokeWidth="1"
android:fillColor="#FFB900"
android:fillColor="@color/app_icon_foreground"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
@ -90,13 +90,13 @@
<path
android:pathData="M77.8,53.601C77.8,53.79 77.799,53.963 77.799,53.963L46.564,53.967C46.755,57.942 50.012,61.117 53.999,61.117C56.367,61.117 58.475,59.995 59.842,58.255L60.739,58.255C59.264,60.419 56.794,61.843 53.999,61.843C49.752,61.843 46.262,58.557 45.872,54.375L45.862,54.261C45.855,54.164 45.855,54.065 45.851,53.966L39.9,53.967C39.769,54.519 39.351,54.953 38.814,55.109C39.569,62.892 46.087,68.996 54,68.996C60.81,68.996 66.592,64.475 68.553,58.254L69.305,58.254C67.32,64.881 61.211,69.721 54,69.721C45.704,69.721 38.873,63.313 38.097,55.149C37.49,55.035 37.009,54.569 36.866,53.967L32.082,53.968C31.949,54.527 31.522,54.965 30.974,55.116C31.752,67.246 41.779,76.875 54,76.875C65.147,76.875 74.471,68.865 76.615,58.254L77.346,58.254C75.19,69.267 65.545,77.6 54,77.6C41.392,77.6 31.048,67.661 30.254,55.146C29.538,55.001 29,54.364 29,53.6C29,52.836 29.539,52.199 30.254,52.054C31.048,39.539 41.391,29.6 54,29.6C67.003,29.601 77.597,40.172 77.791,53.239C77.793,53.36 77.8,53.48 77.8,53.601ZM77.072,53.239C76.878,40.572 66.606,30.327 53.999,30.327C41.778,30.327 31.75,39.957 30.973,52.085C31.522,52.236 31.95,52.677 32.082,53.239L36.863,53.239C37.005,52.635 37.487,52.167 38.095,52.053C38.871,43.888 45.702,37.48 53.999,37.48C61.069,37.48 67.078,42.134 69.182,48.561L68.422,48.561C66.347,42.541 60.667,38.206 53.999,38.206C46.086,38.206 39.568,44.31 38.813,52.092C39.351,52.25 39.77,52.685 39.9,53.239L45.845,53.239C45.852,53.091 45.86,52.961 45.86,52.961C46.186,48.716 49.706,45.36 53.999,45.36C56.623,45.36 58.955,46.617 60.451,48.561L59.513,48.561C58.148,47.045 56.185,46.085 53.999,46.085C50.01,46.085 46.752,49.263 46.562,53.239L46.562,53.239L77.072,53.239Z"
android:strokeWidth="1"
android:fillColor="#FFB900"
android:fillColor="@color/app_icon_foreground"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
<path
android:pathData="M77.8,53.601C77.8,53.79 77.799,53.963 77.799,53.963L46.564,53.967C46.755,57.942 50.012,61.117 53.999,61.117C56.367,61.117 58.475,59.995 59.842,58.255L60.739,58.255C59.264,60.419 56.794,61.843 53.999,61.843C49.752,61.843 46.262,58.557 45.872,54.375L45.862,54.261C45.855,54.164 45.855,54.065 45.851,53.966L39.9,53.967C39.769,54.519 39.351,54.953 38.814,55.109C39.569,62.892 46.087,68.996 54,68.996C60.81,68.996 66.592,64.475 68.553,58.254L69.305,58.254C67.32,64.881 61.211,69.721 54,69.721C45.704,69.721 38.873,63.313 38.097,55.149C37.49,55.035 37.009,54.569 36.866,53.967L32.082,53.968C31.949,54.527 31.522,54.965 30.974,55.116C31.752,67.246 41.779,76.875 54,76.875C65.147,76.875 74.471,68.865 76.615,58.254L77.346,58.254C75.19,69.267 65.545,77.6 54,77.6C41.392,77.6 31.048,67.661 30.254,55.146C29.538,55.001 29,54.364 29,53.6C29,52.836 29.539,52.199 30.254,52.054C31.048,39.539 41.391,29.6 54,29.6C67.003,29.601 77.597,40.172 77.791,53.239C77.793,53.36 77.8,53.48 77.8,53.601ZM77.072,53.239C76.878,40.572 66.606,30.327 53.999,30.327C41.778,30.327 31.75,39.957 30.973,52.085C31.522,52.236 31.95,52.677 32.082,53.239L36.863,53.239C37.005,52.635 37.487,52.167 38.095,52.053C38.871,43.888 45.702,37.48 53.999,37.48C61.069,37.48 67.078,42.134 69.182,48.561L68.422,48.561C66.347,42.541 60.667,38.206 53.999,38.206C46.086,38.206 39.568,44.31 38.813,52.092C39.351,52.25 39.77,52.685 39.9,53.239L45.845,53.239C45.852,53.091 45.86,52.961 45.86,52.961C46.186,48.716 49.706,45.36 53.999,45.36C56.623,45.36 58.955,46.617 60.451,48.561L59.513,48.561C58.148,47.045 56.185,46.085 53.999,46.085C50.01,46.085 46.752,49.263 46.562,53.239L46.562,53.239L77.072,53.239Z"
android:strokeWidth="1"
android:fillColor="#FFB900"
android:fillColor="@color/app_icon_foreground"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
</vector>

View File

@ -126,7 +126,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginEnd="8dp"
tools:text="fitness"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_5"
app:layout_constraintStart_toEndOf="@id/barrier_left_address_column_1"
@ -218,7 +218,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginEnd="8dp"
tools:text="goals"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_6"
app:layout_constraintStart_toEndOf="@id/barrier_left_address_column_2"
@ -322,7 +322,7 @@ text_address_part_3, text_address_part_6, text_address_part_9, text_address_part
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Birthday Height: 510,123"
android:textSize="20dp"
android:textSize="20sp"
android:fontFamily="@font/inconsolata"
app:layout_constraintTop_toBottomOf="@id/receive_address_parts"
app:layout_constraintBottom_toTopOf="@id/text_message"
@ -335,14 +335,15 @@ text_address_part_3, text_address_part_6, text_address_part_9, text_address_part
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Body1"
android:gravity="center"
android:text="empowering\neveryone\nwith\neconomic\nfreedom"
android:text="@string/backup_slogan"
android:textColor="@color/zcashWhite_50"
android:textSize="56dp"
android:alpha="0.03"
app:layout_constraintBottom_toBottomOf="@id/icon_logo"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/icon_logo" />
app:layout_constraintTop_toTopOf="@id/icon_logo"
tools:ignore="SpUsage" />
<ImageView
android:id="@+id/icon_logo"
@ -356,7 +357,8 @@ text_address_part_3, text_address_part_6, text_address_part_9, text_address_part
app:layout_constraintVertical_bias="0.33333"
app:layout_constraintWidth_percent="0.4053398058"
android:visibility="invisible"
app:srcCompat="@drawable/ic_logo_landing" />
app:srcCompat="@drawable/ic_logo_landing"
android:contentDescription="@string/content_description_backup_zcash_logo" />
<!-- Choose release names from here https://en.wikipedia.org/wiki/List_of_woods -->
<TextView
@ -367,7 +369,7 @@ text_address_part_3, text_address_part_6, text_address_part_9, text_address_part
android:gravity="center"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:text="Store these backup words securely."
android:text="@string/backup_instruction_store_words"
android:textColor="@color/zcashWhite"
app:layout_constraintBottom_toTopOf="@id/guideline_buttons"
app:layout_constraintEnd_toEndOf="parent"
@ -378,7 +380,7 @@ text_address_part_3, text_address_part_6, text_address_part_9, text_address_part
android:id="@+id/button_positive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Verify"
android:text="@string/backup_button_primary"
android:textColor="@color/text_dark"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@ -54,7 +54,8 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:srcCompat="@drawable/ic_arrow_back_black_24dp" />
app:srcCompat="@drawable/ic_arrow_back_black_24dp"
android:contentDescription="@string/content_description_history_back" />
<View
android:id="@+id/back_button_hit_area"
@ -66,14 +67,15 @@
app:layout_constraintHorizontal_bias="0.01"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.045" />
app:layout_constraintVertical_bias="0.045"
android:focusable="true" />
<TextView
android:id="@+id/text_balance_available"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Updating"
android:text="@string/history_balance_updating"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
android:visibility="visible"
@ -87,8 +89,8 @@
android:id="@+id/label_balance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:text="Available"
android:layout_marginStart="4dp"
android:text="@string/history_balance_available"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorPrimary"
app:layout_constraintBaseline_toBaselineOf="@id/text_balance_available"
@ -99,7 +101,7 @@
android:id="@+id/text_balance_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="(enter an amount to send)"
android:text="@string/history_instruction_enter_amount"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
android:textColor="@color/text_light_dimmed"
app:layout_constraintBottom_toBottomOf="@id/back_button"
@ -124,9 +126,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="Your Wallet History"
android:text="@string/history_header_transactions"
android:textColor="@color/text_light"
android:textSize="22dp"
android:textSize="22sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@ -135,7 +137,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Shielded address: "
android:paddingEnd="8dp"
android:text="@string/history_address_label"
android:textColor="@color/text_light_dimmed"
app:layout_constraintStart_toStartOf="@id/text_header_title"
app:layout_constraintTop_toBottomOf="@+id/text_header_title" />
@ -160,7 +163,8 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.15"
app:layout_constraintStart_toEndOf="@id/text_address"
app:layout_constraintTop_toTopOf="@id/label_address" />
app:layout_constraintTop_toTopOf="@id/label_address"
android:contentDescription="@string/content_description_history_copy" />
</androidx.constraintlayout.widget.ConstraintLayout>
<View
@ -176,9 +180,9 @@
android:id="@+id/empty_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No history yet."
android:text="@string/history_empty_text"
android:textColor="@color/text_light"
android:textSize="18dp"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@ -46,7 +46,7 @@
android:id="@+id/text_balance_available"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Updating"
android:text="@string/home_balance_updating"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
android:visibility="visible"
@ -59,8 +59,8 @@
android:id="@+id/label_balance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:text="Available"
android:layout_marginStart="4dp"
android:text="@string/home_balance_available"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorPrimary"
android:visibility="gone"
@ -72,7 +72,7 @@
android:id="@+id/text_balance_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="(enter an amount to send)"
android:text="@string/home_instruction_enter_amount"
android:visibility="gone"
tools:visibility="visible"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
@ -97,7 +97,8 @@
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toTopOf="@id/guide_keys"
app:layout_constraintVertical_chainStyle="spread_inside"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent" />
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_2"
@ -111,7 +112,8 @@
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toTopOf="@id/guide_keys"
app:layout_constraintVertical_chainStyle="spread_inside"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent" />
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_3"
@ -124,7 +126,8 @@
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintTop_toTopOf="@id/guide_keys"
app:layout_constraintVertical_chainStyle="spread_inside"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent" />
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_4"
@ -137,7 +140,8 @@
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/button_number_pad_1"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent" />
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_5"
@ -150,7 +154,8 @@
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/button_number_pad_2"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent" />
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_6"
@ -162,7 +167,8 @@
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/button_number_pad_3"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent" />
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_7"
@ -175,7 +181,8 @@
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/button_number_pad_4"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent" />
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_8"
@ -188,7 +195,8 @@
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/button_number_pad_5"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent" />
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_9"
@ -200,7 +208,8 @@
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/button_number_pad_6"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent" />
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_decimal"
@ -208,7 +217,7 @@
android:layout_height="0dp"
style="@style/Zcash.TextView.NumberPad"
android:paddingBottom="2dp"
android:text="."
android:text="@string/key_decimal"
app:layout_constraintBottom_toBottomOf="@id/guide_keys"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintHorizontal_chainStyle="packed"
@ -227,14 +236,15 @@
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/button_number_pad_8"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent" />
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_number_pad_back"
android:layout_width="0dp"
android:layout_height="0dp"
style="@style/Zcash.TextView.NumberPad"
android:text="&lt;"
android:text="@string/key_backspace"
app:layout_constraintBottom_toBottomOf="@id/guide_keys"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="@id/guide_keys"
@ -248,7 +258,8 @@
android:clickable="true"
android:background="#D0000000"
tools:visibility="gone"
android:elevation="5dp" />
android:elevation="5dp"
android:focusable="true" />
<!-- -->
<!-- Upper Layer -->
@ -284,7 +295,7 @@
android:paddingStart="56dp"
android:paddingEnd="56dp"
android:textColor="@color/selector_button_text_dark"
android:textSize="16dp"
android:textSize="16sp"
app:layout_constraintTop_toTopOf="@id/lottie_button_loading"
app:layout_constraintBottom_toBottomOf="@id/lottie_button_loading"
app:layout_constraintStart_toStartOf="parent"
@ -304,7 +315,8 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/icon_profile"
app:layout_constraintWidth_percent="0.0887"
app:srcCompat="@drawable/ic_address_qr" />
app:srcCompat="@drawable/ic_address_qr"
android:contentDescription="@string/scan_address_title" />
<ImageView
android:id="@+id/icon_profile"
@ -320,7 +332,8 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.064"
app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_account_circle" />
app:srcCompat="@drawable/ic_account_circle"
android:contentDescription="@string/content_description_home_icon_profile" />
<View
android:id="@+id/hit_area_receive"
@ -385,7 +398,7 @@
android:includeFontPadding="false"
tools:text="$0"
android:textAppearance="@style/Zcash.TextAppearance.Zec"
android:textSize="72dp"
android:textSize="72sp"
android:maxLines="1"
android:paddingStart="16dp"
android:paddingEnd="16dp"
@ -408,8 +421,9 @@
android:background="@drawable/background_banner"
android:paddingBottom="8dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="8dp"
android:text="No Balance"
android:text="@string/home_no_balance"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
app:layout_constraintEnd_toEndOf="@id/icon_profile"
@ -422,7 +436,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="Fund Now"
android:text="@string/home_instruction_fund_now"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorPrimary"
app:layout_constraintBaseline_toBaselineOf="@id/text_banner_message"

View File

@ -49,7 +49,7 @@
android:gravity="center"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:text="Welcome to the ECC Wallet!"
android:text="@string/landing_title"
android:textColor="@color/zcashWhite"
app:layout_constraintBottom_toTopOf="@id/guideline_buttons"
app:layout_constraintEnd_toEndOf="parent"
@ -61,7 +61,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Zcash.Button.OutlinedButton"
android:text="Restore"
android:text="@string/landing_button_secondary"
android:textColor="@color/text_light"
app:layout_constraintEnd_toStartOf="@id/button_positive"
app:layout_constraintHorizontal_chainStyle="packed"
@ -73,7 +73,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="New"
android:text="@string/landing_button_primary"
android:textColor="@color/text_dark"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/button_negative"

View File

@ -62,7 +62,8 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_cancel" />
app:srcCompat="@drawable/ic_cancel"
android:contentDescription="@string/content_description_profile_back" />
<ImageView
android:id="@+id/icon_settings"
@ -78,7 +79,8 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_settings" />
app:srcCompat="@drawable/ic_settings"
android:contentDescription="@string/content_description_profile_settings" />
<ImageView
android:id="@+id/icon_profile"
@ -92,7 +94,8 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.212"
app:layout_constraintWidth_percent="0.4"
app:srcCompat="@drawable/ic_profile_zebra_01" />
app:srcCompat="@drawable/ic_profile_zebra_01"
android:contentDescription="@string/content_description_profile_zebra" />
<View
android:id="@+id/hit_area_settings"
@ -138,7 +141,8 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.15"
app:layout_constraintStart_toEndOf="@id/text_address"
app:layout_constraintTop_toTopOf="@id/text_address" />
app:layout_constraintTop_toTopOf="@id/text_address"
android:contentDescription="@string/content_description_profile_copy" />
<View
android:id="@+id/hit_area_address"
@ -214,7 +218,8 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/button_logs" />
app:layout_constraintTop_toBottomOf="@id/button_logs"
tools:ignore="RtlSymmetry" />
<TextView
android:id="@+id/text_version"
@ -222,7 +227,7 @@
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:elevation="6dp"
android:text="@string/profile_app_version"
tools:text="@string/profile_app_version"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light_dimmed"
app:layout_constraintBaseline_toBaselineOf="@id/text_banner_message"

View File

@ -39,30 +39,29 @@
tools:background="@color/zcashRed" />
<!-- <ImageView-->
<!-- android:id="@+id/scan_frame"-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="0dp"-->
<!-- android:scaleType="centerCrop"-->
<!-- android:src="@drawable/ic_scan_frame"-->
<!-- app:layout_constraintDimensionRatio="H,1:1"-->
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
<!-- app:layout_constraintEnd_toStartOf="@id/spacer_bottom_right"-->
<!-- app:layout_constraintStart_toEndOf="@id/spacer_bottom_left"-->
<!-- app:layout_constraintTop_toTopOf="parent"-->
<!-- app:layout_constraintVertical_bias="0.3" />-->
<!-- <ImageView-->
<!-- android:id="@+id/scan_frame"-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="0dp"-->
<!-- android:scaleType="centerCrop"-->
<!-- android:src="@drawable/ic_scan_frame"-->
<!-- app:layout_constraintDimensionRatio="H,1:1"-->
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
<!-- app:layout_constraintEnd_toStartOf="@id/spacer_bottom_right"-->
<!-- app:layout_constraintStart_toEndOf="@id/spacer_bottom_left"-->
<!-- app:layout_constraintTop_toTopOf="parent"-->
<!-- app:layout_constraintVertical_bias="0.3" />-->
<ImageView
android:id="@+id/background_qr"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/content_description_receive_qr_background"
android:scaleType="fitXY"
android:src="@drawable/ic_background_qr"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toStartOf="@id/spacer_bottom_right"
app:layout_constraintStart_toEndOf="@id/spacer_bottom_left"
app:layout_constraintTop_toTopOf="parent"
@ -73,21 +72,23 @@
android:id="@+id/receive_qr_code"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/content_description_receive_qr_code"
android:onClick="copyAddress"
android:scaleType="fitCenter"
tools:visibility="visible"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintWidth_percent="0.625"
app:layout_constraintStart_toStartOf="@id/background_qr"
app:layout_constraintEnd_toEndOf="@id/background_qr"
app:layout_constraintTop_toTopOf="@id/background_qr"
app:layout_constraintBottom_toBottomOf="@id/background_qr"
tools:background="@color/zcashWhite" />
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="@id/background_qr"
app:layout_constraintStart_toStartOf="@id/background_qr"
app:layout_constraintTop_toTopOf="@id/background_qr"
app:layout_constraintWidth_percent="0.625"
tools:background="@color/zcashWhite"
tools:visibility="visible" />
<ImageView
android:id="@+id/icon_qr_logo"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/content_description_receive_qr_logo"
android:onClick="copyAddress"
android:scaleType="fitCenter"
android:src="@drawable/ic_shield_address"
@ -102,12 +103,12 @@
android:id="@+id/receive_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/address_label_shielded"
android:drawableEnd="@drawable/ic_content_copy"
android:drawablePadding="16dp"
android:text="@string/receive_address_label_shielded"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
android:textSize="20dp"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/spacer_title" />
@ -119,10 +120,10 @@
android:layout_height="0dp"
android:layout_marginTop="24dp"
android:onClick="copyAddress"
app:layout_constraintStart_toStartOf="@id/background_qr"
app:layout_constraintEnd_toEndOf="@id/background_qr"
app:layout_constraintTop_toBottomOf="@id/background_qr"
app:layout_constraintBottom_toBottomOf="@id/receive_address_parts"
app:layout_constraintEnd_toEndOf="@id/background_qr"
app:layout_constraintStart_toStartOf="@id/background_qr"
app:layout_constraintTop_toBottomOf="@id/background_qr"
tools:background="@color/spacer" />
<!-- Back Button -->
@ -132,14 +133,15 @@
android:layout_height="0dp"
android:tint="@color/text_light_dimmed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.088"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintWidth_percent="0.08"
app:layout_constraintVertical_bias="0.065"
app:srcCompat="@drawable/ic_cancel" />
app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_cancel"
android:contentDescription="@string/content_description_receive_back" />
<View
android:id="@+id/back_button_hit_area"
@ -147,6 +149,7 @@
android:layout_height="68dp"
android:layout_marginStart="24dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
@ -157,17 +160,16 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:autoSizeTextType="uniform"
android:maxLines="1"
android:text="@string/receive_address_title"
android:textColor="@color/text_light"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="@id/back_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/back_button_hit_area"
app:layout_constraintTop_toTopOf="@id/back_button" />
<!-- Address parts -->
<!-- Someday, there will be an advanced VirtualLayout that helps us do this without nesting but for now, this seems to be the only clean way to center all the fields -->
@ -180,95 +182,95 @@
app:layout_constraintEnd_toEndOf="@id/receive_title"
app:layout_constraintStart_toStartOf="@id/receive_title"
app:layout_constraintTop_toBottomOf="@id/receive_title">
<TextView
android:id="@+id/text_address_part_1"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=" ztestsaplin"
style="@style/Zcash.TextAppearance.AddressPart"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintVertical_chainStyle="packed" />
app:layout_constraintVertical_chainStyle="packed"
tools:text=" ztestsaplin" />
<TextView
android:id="@+id/text_address_part_3"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text=" jceuu9s2p6t"
style="@style/Zcash.TextAppearance.AddressPart"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_5"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_1" />
app:layout_constraintTop_toBottomOf="@id/text_address_part_1"
tools:text=" jceuu9s2p6t" />
<TextView
android:id="@+id/text_address_part_5"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text=" 7u7uarqls7d"
style="@style/Zcash.TextAppearance.AddressPart"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_7"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_3" />
app:layout_constraintTop_toBottomOf="@id/text_address_part_3"
tools:text=" 7u7uarqls7d" />
<TextView
android:id="@+id/text_address_part_7"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text=" rzq85xggu56"
style="@style/Zcash.TextAppearance.AddressPart"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
app:layout_constraintTop_toBottomOf="@id/text_address_part_5" />
app:layout_constraintTop_toBottomOf="@id/text_address_part_5"
tools:text=" rzq85xggu56" />
<TextView
android:id="@+id/text_address_part_2"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:text=" g1mwjzlg62j"
style="@style/Zcash.TextAppearance.AddressPart"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_4"
app:layout_constraintStart_toEndOf="@id/barrier_left_address_column"
app:layout_constraintTop_toTopOf="@id/text_address_part_1" />
app:layout_constraintTop_toTopOf="@id/text_address_part_1"
tools:text=" g1mwjzlg62j" />
<TextView
android:id="@+id/text_address_part_4"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text=" wns6qxwec6v"
style="@style/Zcash.TextAppearance.AddressPart"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_6"
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
app:layout_constraintTop_toBottomOf="@id/text_address_part_2" />
app:layout_constraintTop_toBottomOf="@id/text_address_part_2"
tools:text=" wns6qxwec6v" />
<TextView
android:id="@+id/text_address_part_6"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text=" gtg3tpgqxjd"
style="@style/Zcash.TextAppearance.AddressPart"
app:layout_constraintBottom_toTopOf="@+id/text_address_part_8"
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
app:layout_constraintTop_toBottomOf="@id/text_address_part_4" />
app:layout_constraintTop_toBottomOf="@id/text_address_part_4"
tools:text=" gtg3tpgqxjd" />
<TextView
android:id="@+id/text_address_part_8"
style="@style/Zcash.TextAppearance.AddressPart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text=" k904xderng6"
style="@style/Zcash.TextAppearance.AddressPart"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
app:layout_constraintTop_toBottomOf="@id/text_address_part_6" />
app:layout_constraintTop_toBottomOf="@id/text_address_part_6"
tools:text=" k904xderng6" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_left_address_column"

View File

@ -141,6 +141,7 @@
android:id="@+id/scan_frame"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/content_description_scan_frame"
android:scaleType="centerCrop"
android:src="@drawable/ic_scan_frame"
app:layout_constraintBottom_toBottomOf="parent"
@ -155,13 +156,13 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="32dp"
android:ellipsize="end"
android:fontFamily="@font/inconsolata"
android:gravity="center"
android:textColor="@color/text_light_dimmed"
android:textSize="16dp"
android:ellipsize="end"
android:paddingBottom="16dp"
android:paddingTop="8dp"
android:textColor="@color/text_light_dimmed"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="@id/scan_frame"
app:layout_constraintStart_toStartOf="@id/scan_frame"
app:layout_constraintTop_toBottomOf="@id/scan_frame"
@ -169,25 +170,28 @@
<ImageView
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="@color/text_light"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/content_description_scan_back"
android:tint="@color/text_light_dimmed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.05"
app:layout_constraintHorizontal_bias="0.088"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:srcCompat="@drawable/ic_close_black_24dp" />
app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_cancel" />
<View
android:id="@+id/back_button_hit_area"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_width="68dp"
android:layout_height="68dp"
android:layout_marginStart="24dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.01"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.045" />
@ -197,10 +201,10 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:autoSizeTextType="uniform"
android:maxLines="1"
android:text="@string/scan_address_title"
android:textColor="@color/text_light"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="@id/back_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/back_button_hit_area"

View File

@ -19,6 +19,7 @@
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/content_description_send_back"
android:tint="@color/text_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@ -33,6 +34,7 @@
android:layout_width="56dp"
android:layout_height="56dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.01"
@ -52,7 +54,6 @@
android:paddingEnd="16dp"
android:paddingStart="16dp"
android:paddingTop="0dp"
android:text="$20.1"
android:textAppearance="@style/Zcash.TextAppearance.Zec"
android:textSize="200sp"
app:autoSizeMaxTextSize="40sp"
@ -62,14 +63,15 @@
app:layout_constraintEnd_toStartOf="@id/button_send"
app:layout_constraintStart_toEndOf="@id/spacer_title"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
app:layout_constraintVertical_chainStyle="packed"
tools:text="$20.1" />
<TextView
android:id="@+id/text_banner_message_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="6dp"
android:text="from your "
android:text="@string/send_fund_source_prefix"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="@color/text_light"
app:layout_constraintBottom_toTopOf="@id/guideline_content_top"
@ -83,7 +85,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="6dp"
android:text="shielded"
android:text="@string/send_fund_source_highlight"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="@color/colorPrimary"
app:layout_constraintBaseline_toBaselineOf="@id/text_banner_message_start"
@ -95,7 +97,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="6dp"
android:text=" wallet"
android:text="@string/send_fund_source_suffix"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="@color/text_light"
app:layout_constraintBaseline_toBaselineOf="@id/text_banner_message_start"
@ -107,6 +109,7 @@
android:id="@+id/image_down_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/content_description_send_arrow"
android:tint="@color/colorPrimary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/text_banner_message_end"
@ -127,11 +130,16 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/button_send"
android:layout_width="74dp"
android:layout_width="wrap_content"
android:layout_height="38dp"
android:text="Send"
android:textColor="@color/text_dark"
android:backgroundTint="@color/selector_primary_button_activatable"
android:maxLines="1"
android:paddingEnd="2dp"
android:paddingStart="2dp"
android:text="@string/send_button_primary"
android:textColor="@color/text_dark"
app:autoSizeMinTextSize="6sp"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="@id/back_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.95"
@ -143,16 +151,16 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:autoSizeMaxTextSize="12sp"
app:autoSizeMinTextSize="6sp"
app:autoSizeTextType="uniform"
android:fontFamily="@font/inconsolata"
android:gravity="center"
android:maxLines="1"
android:paddingEnd="8dp"
android:paddingStart="8dp"
android:textColor="@color/zcashRed"
android:maxLines="1"
android:textSize="14sp"
android:gravity="center"
app:autoSizeMaxTextSize="12sp"
app:autoSizeMinTextSize="6sp"
app:autoSizeTextType="uniform"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_banner_message_start"
@ -176,9 +184,9 @@
android:id="@+id/spacer_lower_content"
android:layout_width="1dp"
android:layout_height="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.04"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/check_include_address" />
<!-- Input: Address -->
@ -186,7 +194,7 @@
android:id="@+id/text_layout_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="To"
android:hint="@string/send_address_hint"
android:theme="@style/Zcash.Overlay.TextInputLayout"
app:endIconDrawable="@drawable/ic_qr_scan"
app:endIconMode="custom"
@ -215,7 +223,7 @@
android:id="@+id/text_layout_memo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Memo"
android:hint="@string/send_memo_hint"
android:theme="@style/Zcash.Overlay.TextInputLayout"
app:endIconMode="clear_text"
app:layout_constraintEnd_toEndOf="parent"
@ -239,17 +247,17 @@
tools:text="WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" />
</com.google.android.material.textfield.TextInputLayout>
<!-- <ImageView-->
<!-- android:id="@+id/clear_memo"-->
<!-- android:layout_width="24dp"-->
<!-- android:layout_height="24dp"-->
<!-- android:layout_marginEnd="10dp"-->
<!-- android:layout_marginTop="6dp"-->
<!-- android:elevation="6dp"-->
<!-- android:src="@drawable/ic_close_black_24dp"-->
<!-- android:tint="@color/text_light"-->
<!-- app:layout_constraintEnd_toEndOf="@id/text_layout_memo"-->
<!-- app:layout_constraintTop_toTopOf="@id/text_layout_memo" />-->
<!-- <ImageView-->
<!-- android:id="@+id/clear_memo"-->
<!-- android:layout_width="24dp"-->
<!-- android:layout_height="24dp"-->
<!-- android:layout_marginEnd="10dp"-->
<!-- android:layout_marginTop="6dp"-->
<!-- android:elevation="6dp"-->
<!-- android:src="@drawable/ic_close_black_24dp"-->
<!-- android:tint="@color/text_light"-->
<!-- app:layout_constraintEnd_toEndOf="@id/text_layout_memo"-->
<!-- app:layout_constraintTop_toTopOf="@id/text_layout_memo" />-->
<!-- Spacer to help position checkbox under the memo line -->
<Space
@ -270,7 +278,7 @@
android:padding="0dp"
android:scaleX="0.84"
android:scaleY="0.84"
android:text="include reply-to"
android:text="@string/send_checkbox_include_address"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="@color/text_light_dimmed"
android:textSize="14sp"
@ -298,7 +306,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="1dp"
android:text="On clipboard"
android:text="@string/send_history_clipboard"
android:textAllCaps="true"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintEnd_toEndOf="parent"
@ -320,6 +328,7 @@
android:id="@+id/image_shield"
android:layout_width="16dp"
android:layout_height="16dp"
android:contentDescription="@string/content_description_send_shield"
android:src="@drawable/ic_shielded"
android:tint="@color/colorPrimary"
app:layout_constraintBottom_toBottomOf="@id/clipboard_address_label"
@ -333,7 +342,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="Unknown"
android:text="@string/unknown"
android:textColor="@color/colorPrimary"
app:layout_constraintBottom_toTopOf="@id/clipboard_address"
app:layout_constraintStart_toEndOf="@id/image_shield"
@ -344,16 +353,17 @@
android:id="@+id/clipboard_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="zasdfaksfjaslfjaslfkjaslk;kfjaslkfjasld;kfjaslfjdasflja"
android:textColor="@color/text_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@id/image_shield"
app:layout_constraintTop_toBottomOf="@id/clipboard_address_label" />
app:layout_constraintTop_toBottomOf="@id/clipboard_address_label"
tools:text="zasdfaksfjaslfjaslfkjaslk;kfjaslkfjasld;kfjaslfjdasflja" />
<ImageView
android:id="@+id/image_clipboard_address_selected"
android:layout_width="22dp"
android:layout_height="22dp"
android:contentDescription="@string/content_description_send_selected"
android:src="@drawable/ic_check_shielded"
android:tint="@color/colorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
@ -383,7 +393,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="1dp"
android:text="last used"
android:text="@string/send_history_last"
android:textAllCaps="true"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintEnd_toEndOf="parent"
@ -403,6 +413,7 @@
android:id="@+id/image_last_used_shield"
android:layout_width="16dp"
android:layout_height="16dp"
android:contentDescription="@string/content_description_send_shield"
android:src="@drawable/ic_shielded"
android:tint="@color/colorPrimary"
app:layout_constraintBottom_toBottomOf="@id/last_used_address_label"
@ -417,7 +428,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="16dp"
android:text="Unknown"
android:text="@string/unknown"
android:textColor="@color/colorPrimary"
app:layout_constraintBottom_toTopOf="@id/last_used_address"
app:layout_constraintStart_toEndOf="@id/image_last_used_shield"
@ -428,16 +439,17 @@
android:id="@+id/last_used_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="zasdfaksfjaslfjaslfkjaslk;kfjaslkfjasld;kfjaslfjdasflja"
android:textColor="@color/text_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@id/image_last_used_shield"
app:layout_constraintTop_toBottomOf="@id/last_used_address_label" />
app:layout_constraintTop_toBottomOf="@id/last_used_address_label"
tools:text="zasdfaksfjaslfjaslfkjaslk;kfjaslkfjasld;kfjaslfjdasflja" />
<ImageView
android:id="@+id/image_last_used_address_selected"
android:layout_width="22dp"
android:layout_height="22dp"
android:contentDescription="@string/content_description_send_selected"
android:src="@drawable/ic_check_shielded"
android:tint="@color/colorPrimary"
android:visibility="gone"
@ -467,6 +479,7 @@
android:id="@+id/image_scan_qr"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/content_description_send_scan_qr"
android:paddingBottom="24dp"
android:paddingEnd="1dp"
android:paddingStart="6dp"

View File

@ -25,36 +25,40 @@
<!-- Back Button -->
<ImageView
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/content_description_send_final_back"
android:tint="@color/zcashBlack_87"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.05"
app:layout_constraintHorizontal_bias="0.088"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:srcCompat="@drawable/ic_close_black_24dp"
app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_cancel"
tools:visibility="visible" />
<View
android:id="@+id/back_button_hit_area"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_width="68dp"
android:layout_height="68dp"
android:layout_marginStart="24dp"
android:clickable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.01"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.045" />
app:layout_constraintVertical_bias="0.045"
android:focusable="true" />
<TextView
android:id="@+id/text_confirmation"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Headline5"
android:autoSizeTextType="uniform"
app:autoSizeTextType="uniform"
android:gravity="center"
android:maxLines="3"
android:paddingEnd="16dp"
@ -87,15 +91,14 @@
android:id="@+id/error_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Oops! Failed to send due to insufficient funds!"
android:textColor="@color/text_dark"
android:gravity="center"
tools:text="Oops! Failed to send due to insufficient funds!"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintTop_toBottomOf="@id/lottie_sending"
android:textColor="@color/text_dark"
app:layout_constraintBottom_toTopOf="@id/button_primary"
app:layout_constraintStart_toStartOf="@id/button_primary"
app:layout_constraintEnd_toEndOf="@id/button_primary"
/>
app:layout_constraintStart_toStartOf="@id/button_primary"
app:layout_constraintTop_toBottomOf="@id/lottie_sending" />
<!-- <TextView-->
<!-- android:id="@+id/text_status"-->
<!-- android:layout_width="wrap_content"-->
@ -141,9 +144,9 @@
android:layout_height="wrap_content"
style="@style/Zcash.Button.OutlinedButton"
android:padding="20dp"
android:translationY="-6dp"
android:text="@string/cancel"
android:textColor="@color/text_dark"
android:translationY="-6dp"
app:layout_constraintBottom_toTopOf="@id/button_secondary"
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintStart_toStartOf="@id/guide_keys"

View File

@ -29,6 +29,7 @@
android:id="@+id/icon_exit"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="@string/content_description_settings_back"
android:elevation="6dp"
android:tint="@color/text_light_dimmed"
app:layout_constraintBottom_toBottomOf="parent"
@ -47,10 +48,10 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:autoSizeTextType="uniform"
android:maxLines="1"
android:text="@string/settings_change_lightwalletd_server"
android:textColor="@color/text_light"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="@id/icon_exit"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/hit_area_exit"
@ -138,17 +139,18 @@
android:layout_height="match_parent"
android:background="@color/zcashWhite_24"
android:clickable="true"
android:elevation="8dp"
android:focusableInTouchMode="true"
android:elevation="8dp" />
android:focusable="true" />
<ProgressBar
android:id="@+id/loading_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
app:layout_constraintTop_toTopOf="@id/button_reset"
app:layout_constraintBottom_toBottomOf="@id/button_reset"
app:layout_constraintStart_toStartOf="@id/icon_exit"/>
app:layout_constraintStart_toStartOf="@id/icon_exit"
app:layout_constraintTop_toTopOf="@id/button_reset" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_loading"
@ -157,5 +159,4 @@
app:constraint_referenced_ids="loading_progress,loading_view" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -71,6 +71,7 @@
android:id="@+id/close_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/content_description_transaction_details_back"
android:tint="@color/zcashWhite_40"
app:layout_constraintBottom_toTopOf="@id/text_timestamp"
app:layout_constraintEnd_toEndOf="parent"
@ -84,6 +85,7 @@
android:layout_width="56dp"
android:layout_height="56dp"
android:clickable="true"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.01"
@ -108,13 +110,13 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:autoSizeMaxTextSize="18sp"
android:autoSizeMinTextSize="6dp"
android:autoSizeTextType="uniform"
android:gravity="bottom"
android:maxLines="1"
android:textColor="@color/tx_text_light_dimmed"
android:textSize="18sp"
app:autoSizeMaxTextSize="18sp"
app:autoSizeMinTextSize="6dp"
app:autoSizeTextType="uniform"
app:layout_constraintBaseline_toBaselineOf="@id/text_block_height"
app:layout_constraintEnd_toStartOf="@id/text_block_height_prefix"
app:layout_constraintHorizontal_bias="0"
@ -204,7 +206,8 @@
app:layout_constraintEnd_toEndOf="@id/top_box_icon_background"
app:layout_constraintHeight_percent="0.0408"
app:layout_constraintStart_toStartOf="@id/top_box_icon_background"
app:layout_constraintTop_toTopOf="@id/top_box_icon_background" />
app:layout_constraintTop_toTopOf="@id/top_box_icon_background"
android:contentDescription="@string/content_description_transaction_details_direction" />
<TextView
android:id="@+id/top_box_label"
@ -294,7 +297,8 @@
app:layout_constraintEnd_toEndOf="@id/space_spots_memo"
app:layout_constraintStart_toStartOf="@id/space_spots_memo"
app:layout_constraintTop_toTopOf="@id/subway_label_memo"
tools:visibility="visible" />
tools:visibility="visible"
android:contentDescription="@string/content_description_transaction_details_memo" />
<View
android:id="@+id/subway_spot_address"
@ -332,8 +336,9 @@
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:textColor="@color/tx_text_light_dimmed"
android:textSize="18dp"
android:textSize="18sp"
android:visibility="gone"
tools:visibility="visible"
app:layout_constraintStart_toStartOf="@id/subway_line"
app:layout_constraintTop_toTopOf="@id/subway_line"
tools:text="+0.0001 network fee" />
@ -346,7 +351,7 @@
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:textColor="@color/tx_text_light_dimmed"
android:textSize="18dp"
android:textSize="18sp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/subway_line"
app:layout_constraintTop_toBottomOf="@id/subway_label_fee"
@ -359,8 +364,9 @@
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:paddingEnd="8dp"
android:paddingStart="8dp"
android:text="@string/transaction_with_memo"
android:textSize="18dp"
android:textSize="18sp"
android:visibility="invisible"
app:layout_constraintStart_toStartOf="@id/subway_line"
app:layout_constraintTop_toBottomOf="@id/subway_label_source"
@ -378,7 +384,7 @@
android:maxLines="3"
android:scrollbars="vertical"
android:textColor="@color/tx_text_light_dimmed"
android:textSize="18dp"
android:textSize="18sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintStart_toStartOf="@id/subway_line"
@ -394,7 +400,8 @@
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/spacer_memo_icon"
app:layout_constraintStart_toEndOf="@id/spacer_memo_icon"
app:layout_constraintTop_toTopOf="@id/spacer_memo_icon" />
app:layout_constraintTop_toTopOf="@id/spacer_memo_icon"
android:contentDescription="@string/content_description_transaction_details_memo_icon" />
<View
android:id="@+id/hit_area_memo_subway"
@ -429,7 +436,7 @@
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:textColor="@color/text_light"
android:textSize="18dp"
android:textSize="18sp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/subway_line"
app:layout_constraintTop_toBottomOf="@id/subway_label_memo"
@ -442,7 +449,7 @@
android:layout_marginBottom="24dp"
android:layout_marginStart="16dp"
android:textColor="@color/tx_text_light_dimmed"
android:textSize="18dp"
android:textSize="18sp"
app:layout_constraintBottom_toTopOf="@id/bottom_box_background"
app:layout_constraintStart_toStartOf="@id/subway_line"
tools:text="confirmed" />
@ -477,7 +484,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="Total Spent"
android:text="@string/transaction_details_total"
android:textColor="@color/tx_primary"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/bottom_box_value"
@ -495,7 +502,7 @@
android:layout_marginEnd="8dp"
android:layout_marginTop="4dp"
android:maxLines="1"
android:text="$4.32"
tools:text="$4.32"
android:textAppearance="@style/Zcash.TextAppearance.Zec"
android:textColor="@color/text_light"
android:textSize="36sp"
@ -519,7 +526,7 @@
style="@style/Zcash.Button.OutlinedButton"
android:gravity="center"
android:padding="12dp"
android:text="Explore"
android:text="@string/transaction_details_button_explore"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/tx_text_light_dimmed_less"
app:icon="@drawable/ic_baseline_launch_24"
@ -542,6 +549,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="icon_memo, hit_area_memo_icon, spacer_memo_icon" />
app:constraint_referenced_ids="icon_memo, hit_area_memo_icon" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -27,26 +27,26 @@
android:layout_width="1dp"
android:layout_height="0dp"
android:background="@color/text_light_dimmed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/divider_right"
android:layout_width="1dp"
android:layout_height="0dp"
android:background="@color/text_light_dimmed"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/indicator"
android:layout_width="3dp"
android:layout_height="60dp"
android:layout_marginStart="16dp"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:background="@drawable/background_indicator_inbound"
app:layout_constraintBottom_toBottomOf="parent"
@ -59,6 +59,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:contentDescription="@string/content_description_transaction_shield"
android:src="@drawable/ic_check_shield"
app:layout_constraintBottom_toTopOf="@id/text_transaction_bottom"
app:layout_constraintStart_toEndOf="@id/indicator"
@ -76,20 +77,21 @@
app:layout_constraintBottom_toTopOf="@id/text_transaction_bottom"
app:layout_constraintStart_toEndOf="@id/indicator"
app:layout_constraintTop_toTopOf="@id/indicator"
app:layout_constraintVertical_chainStyle="packed" />
app:layout_constraintVertical_chainStyle="packed"
android:importantForAccessibility="no"/>
<TextView
android:id="@+id/text_transaction_top"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:maxLines="1"
android:textColor="@color/text_light"
android:textSize="14sp"
android:maxLines="1"
android:autoSizeTextType="uniform"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="@id/image_shield_invisible"
app:layout_constraintStart_toEndOf="@id/image_shield"
app:layout_constraintEnd_toStartOf="@id/text_transaction_amount"
app:layout_constraintStart_toEndOf="@id/image_shield"
app:layout_constraintTop_toTopOf="@id/image_shield_invisible"
app:layout_constraintVertical_chainStyle="packed"
app:layout_goneMarginStart="16dp"
@ -104,26 +106,26 @@
android:ellipsize="end"
android:maxLines="1"
android:paddingEnd="16dp"
android:autoSizeTextType="uniform"
android:text="This is a very long sentence and it better not messup the UI"
android:textColor="@color/text_light_dimmed"
android:textSize="12sp"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="@id/indicator"
app:layout_constraintEnd_toStartOf="@id/text_transaction_amount"
app:layout_constraintStart_toEndOf="@id/indicator"
app:layout_constraintTop_toBottomOf="@id/image_shield_invisible" />
app:layout_constraintTop_toBottomOf="@id/image_shield_invisible"
tools:text="This is a very long sentence and it better not messup the UI" />
<TextView
android:id="@+id/text_transaction_amount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:paddingStart="12dp"
android:autoSizeTextType="uniform"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingStart="12dp"
android:textColor="@color/colorPrimary"
android:textSize="20sp"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="@id/indicator"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/indicator"

View File

@ -90,11 +90,6 @@
android:id="@+id/nav_send"
android:name="cash.z.ecc.android.ui.send.SendFragment"
tools:layout="@layout/fragment_send" >
<action
android:id="@+id/action_nav_send_to_send_final"
app:destination="@id/nav_send_final"
app:exitAnim="@anim/anim_exit_to_left"
app:enterAnim="@anim/anim_enter_from_right"/>
<action
android:id="@+id/action_nav_send_to_nav_scan"
app:destination="@id/nav_scan" />
@ -156,6 +151,11 @@
app:destination="@id/nav_history"
app:popUpTo="@id/nav_home"
app:popUpToInclusive="false"/>
<action
android:id="@+id/action_nav_send_final_to_nav_send"
app:destination="@id/nav_send"
app:popUpTo="@id/nav_send"
app:popUpToInclusive="false"/>
</fragment>

View File

@ -0,0 +1,16 @@
<resources>
<string name="custom_translation_decimal_key">,</string>
<string name="custom_translation_decimal_separator">,</string>
<string name="custom_translation_format_date_time_brief">d/M HH:mma</string>
<!-- Manual -->
<string name="custom_translation_verify">Verifica</string>
<string name="custom_translation_birthday">Fecha inicial: %1$,d</string>
<string name="custom_translation_expecting">Esperando</string>
<string name="custom_translation_syncing">Sincronizando…%1$d %%</string>
<string name="custom_translation_scanning">Escaneando…%1$d %%</string>
<string name="custom_translation_send_confirm">Confirma que quieres enviar</string>
<string name="custom_translation_chars">caracteres</string>
<string name="custom_translation_sending">Enviando</string>
</resources>

View File

@ -0,0 +1,74 @@
<resources>
<string name="translated_app_name">Billetera ECC</string>
<string name="translated_loadingtext">Cargando</string>
<string name="translated_welcometext">Bienvenido</string>
<string name="translated_fiattext">fiduciario</string>
<string name="translated_title_backupseed">Tu semilla de respaldo</string>
<string name="translated_copy_backupseed">Te recomendamos hace respaldarla en papel y obtener un administrador de contraseñas</string>
<string name="translated_feedback_walletbackupstatus">Tu Wallet necesita ser respaldada</string>
<string name="translated_seed_verify">Verifica tu frase de respaldo</string>
<string name="translated_seed_verifytext">Ingresa las palabras faltantes para verificar tu frase de respaldo</string>
<string name="translated_seed_restore">Restaurando desde frase de respaldo</string>
<string name="translated_seed_restoretext">Deberás ingresar todas las 24 palabras de tu frase semilla en orden</string>
<string name="translated_seed_reminder">¡Tus fondos están en riesgo!</string>
<string name="translated_seed_remindertext">Recuerda, en Zcash tu eres el banco. Cualquiera que tenga tu frase semilla podrá acceder a tu wallet</string>
<string name="translated_seed_remindertext2">Deberías respaldar tu wallet inmediatamente. Solo tú puedes hacerlo.</string>
<string name="translated_seed_accepted">¡Frase semilla aceptada! ¡Escaneando!</string>
<string name="translated_seed_birthdaytext">Estamos buscando transacciones de tu wallet en la blockchain. Si nos provees una fecha inicial de tu wallet podemos hacerlo más rápido.</string>
<string name="translated_seed_enterdate">Ingresar fecha</string>
<string name="translated_seed_birthday">Fecha inicial</string>
<string name="translated_button_dontknow">No lo sé</string>
<string name="translated_button_backup">Respaldo</string>
<string name="translated_button_skip">Omitir por ahora</string>
<string name="translated_balance_screen">Pantalla de Balance</string>
<string name="translated_balance_amounttosend">Ingresar un monto para enviar</string>
<string name="translated_balance_expecting">esperando XX ZEC</string>
<string name="translated_balance_available">Disponible</string>
<string name="translated_balance_nofunds">No hay fondos disponibles</string>
<string name="translated_balance_syncing">Sincronizando .... %</string>
<string name="translated_balance_scanning">Escaneando .... %</string>
<string name="translated_button_sendamount">Enviar Monto</string>
<string name="translated_button_wallethistory">Historial de Wallet</string>
<string name="translated_address_screen">Tu dirección</string>
<string name="translated_address_shielded">Tu dirección blindada</string>
<string name="translated_address_transparent">Tu dirección transparente</string>
<string name="translated_feedback_addresscopied">¡Dirección Copiada!</string>
<string name="translated_button_copytoclipboard">Copiar al portapapeles</string>
<string name="translated_profile_screen">Usuario Blindado</string>
<string name="translated_button_feedback">Enviar feedback</string>
<string name="translated_button_backup_wallet">Respaldar Wallet</string>
<string name="translated_button_applicationlogs">Ver Logs de Aplicación</string>
<string name="translated_button_close">Cerrar</string>
<string name="translated_send_screen">Pantalla de Envío</string>
<string name="translated_button_send">Enviar</string>
<string name="translated_send_fromshielded">de tu wallet blindada</string>
<string name="translated_send_fromtransparent">de tu wallet transparente</string>
<string name="translated_send_fromboth">desde ambas wallet</string>
<string name="translated_label_to">A</string>
<string name="translated_feedback_default">Ingresa una dirección Zcash Válida</string>
<string name="translated_feedback_shieldedaddress">Esta es una dirección blindada válida</string>
<string name="translated_feedback_transparentaddress">Esta es una dirección transparente válida</string>
<string name="translated_feedback_sameaddress">¡Cuidado, parece que esta es tu propia dirección!</string>
<string name="translated_feedback_invalidaddress">¡Cuidado, esta dirección no es válida!</string>
<string name="translated_label_memo">Memo</string>
<string name="translated_label_charactercount">caracteres</string>
<string name="translated_label_replyto">incluir remitente</string>
<string name="translated_send_onclipboard">En Portapapeles</string>
<string name="translated_send_lastused">Última Utilizada</string>
<string name="translated_send_unknown">Desconocido</string>
<string name="translated_send_scanqr">Escanear Dirección destinatario</string>
<string name="translated_send_securityauth">Identifícate para enviar</string>
<string name="translated_send_securityauth2">Confirma que quieres enviar XX ZEC a</string>
<string name="translated_send_sending">Enviando XX ZEC a</string>
<string name="translated_send_cancelled">Cancelado</string>
<string name="translated_send_sent">¡Enviado!</string>
<string name="translated_button_done">Listo</string>
<string name="translated_button_seedetails">Ver Detalles</string>
<string name="translated_button_back">Volver</string>
<string name="translated_screen_wallethistory">Historial de tu Wallet</string>
</resources>
<!-- ISSUES:
- button_backup isn't unique
-->

View File

@ -0,0 +1,16 @@
<resources>
<string name="custom_translation_decimal_key">,</string>
<string name="custom_translation_decimal_separator">,</string>
<string name="custom_translation_format_date_time_brief">d/M HH:mma</string>
<!-- Manual -->
<string name="custom_translation_verify">Controlla</string>
<string name="custom_translation_birthday">Birthday: %1$,d</string>
<string name="custom_translation_expecting">Aspettando</string>
<string name="custom_translation_syncing">Downloading…%1$d%%</string>
<string name="custom_translation_scanning">Scanning…%1$d%%</string>
<string name="custom_translation_send_confirm">Conferma che vuoi inviare</string>
<string name="custom_translation_chars">caratteri</string>
<string name="custom_translation_sending">Inviando</string>
</resources>

View File

@ -0,0 +1,63 @@
<resources>
<string name="translated_address_screen">Tuo Indirizzo</string>
<string name="translated_address_shielded">Il tuo indirizzo blindato</string>
<string name="translated_address_transparent">Il tuo indirizzo trasparente</string>
<string name="translated_balance_amounttosend">Inserisci un importo da inviare</string>
<string name="translated_balance_available">Disponibile</string>
<string name="translated_balance_expecting">aspettando %@ ZEC</string>
<string name="translated_balance_nofunds">Nessun fondo disponibile</string>
<string name="translated_balance_screen">Bilancio</string>
<string name="translated_balance_syncing">Sincronizzando %@%%</string>
<string name="translated_button_applicationlogs">Vedere Registri dell\'applicazione</string>
<string name="translated_button_back">ritornare</string>
<string name="translated_button_backup">Fare Backup</string>
<string name="translated_button_backup_wallet">Fare Backup</string>
<string name="translated_button_close">Chiudere</string>
<string name="translated_button_copytoclipboard">copia negli appunti</string>
<string name="translated_button_done">Pronto</string>
<string name="translated_button_dontknow">non lo so</string>
<string name="translated_button_feedback">"Invia feedback</string>
<string name="translated_button_seedetails">Vedi i dettagli</string>
<string name="translated_button_send">Invio</string>
<string name="translated_button_sendamount">Invia importo</string>
<string name="translated_button_skip">Salta per ora</string>
<string name="translated_button_wallethistory">cronologia del wallet</string>
<string name="translated_feedback_default">Inserisci un indirizzo Zcash valido</string>
<string name="translated_feedback_invalidaddress">Attenzione, questo indirizzo non è valido!</string>
<string name="translated_feedback_sameaddress">Attenzione, sembra che questo sia il tuo indirizzo!</string>
<string name="translated_feedback_shieldedaddress">Questo è un indirizzo blindato valido</string>
<string name="translated_feedback_transparentaddress">Questo è un indirizzo trasparente valido</string>
<string name="translated_feedback_walletbackupstatus">La tua Wallet ha bisogno di un backup</string>
<string name="translated_label_charactercount">caratteri</string>
<string name="translated_label_replyto">includere mittente</string>
<string name="translated_label_to">Per</string>
<string name="translated_loadingtext">Caricando</string>
<string name="translated_profile_screen">Utente blindato</string>
<string name="translated_screen_wallethistory">Storia del tuo Wallet</string>
<string name="translated_seed_accepted">Frase seme accettata!</string>
<string name="translated_seed_birthday">Blocco iniziale</string>
<string name="translated_seed_birthdaytext">Stiamo cercando transazioni dal tuo wallet sulla blockchain. Se ci fornisci una data iniziale del tuo Wallet, possiamo farlo più velocemente.</string>
<string name="translated_seed_enterdate">inserici data</string>
<string name="translated_seed_reminder">I tuoi fondi sono a rischio!</string>
<string name="translated_seed_remindertext">Ricorda, in Zcash sei tu la banca. Chiunque abbia la tua frase seme può accedere al tuo Wallet</string>
<string name="translated_seed_remindertext2">Dovresti eseguire immediatamente il backup del tuo portafoglio. Solo tu puoi farlo</string>
<string name="translated_seed_restore">recupero dalla frase di backup</string>
<string name="translated_seed_restoretext">Devi inserire tutte le 24 parole della tua frase seme in ordine</string>
<string name="translated_seed_verify">Controlla la tua frase di backup</string>
<string name="translated_seed_verifytext">Inserisci le parole mancanti per vedere la frase di backup</string>
<string name="translated_send_cancelled">Annullato</string>
<string name="translated_send_fromboth">dalle due wallet</string>
<string name="translated_send_fromshielded">del tuo wallet blindato</string>
<string name="translated_send_fromtransparent">del tuo wallet trasparente</string>
<string name="translated_send_lastused">Ultimo uso</string>
<string name="translated_send_onclipboard">Negli appunti</string>
<string name="translated_send_scanqr">Scansione indirizzo destinatario</string>
<string name="translated_send_screen">Invio</string>
<string name="translated_send_securityauth">Accedi per inviare</string>
<string name="translated_send_securityauth2">Conferma che vuoi inviare %@ ZEC a</string>
<string name="translated_send_sending">Inviando %@ ZEC a</string>
<string name="translated_send_sent">Inviato!</string>
<string name="translated_send_unknown">Sconosciuto</string>
<string name="translated_title_backupseed">Il tuo seme di backup</string>
<string name="translated_welcometext">Benvenuto</string>
</resources>

View File

@ -0,0 +1,16 @@
<resources>
<string name="custom_translation_decimal_key">,</string>
<string name="custom_translation_decimal_separator">,</string>
<string name="custom_translation_format_date_time_brief">d/M HH:mma</string>
<!-- Manual -->
<string name="custom_translation_verify"> 확인</string>
<string name="custom_translation_birthday">생일: %1$,d</string>
<string name="custom_translation_expecting">기대</string>
<string name="custom_translation_syncing">동기화…%1$d %%</string>
<string name="custom_translation_scanning">스캐닝…%1$d %%</string>
<string name="custom_translation_send_confirm">를 보낼 것인지 확인하십시오</string>
<string name="custom_translation_chars">문자</string>
<string name="custom_translation_sending">보내기</string>
</resources>

View File

@ -0,0 +1,74 @@
<resources>
<string name="translated_app_name">ECC 지갑</string>
<string name="translated_loadingtext">로딩</string>
<string name="translated_welcometext">환영</string>
<string name="translated_fiattext">₩ (currency = 통화)</string>
<string name="translated_title_backupseed">당신의 백업 시드</string>
<string name="translated_copy_backupseed">종이 백업과 비밀번호 보관소를 권장합니다.</string>
<string name="translated_feedback_walletbackupstatus">지갑을 백업해야합니다</string>
<string name="translated_seed_verify">백업 확인</string>
<string name="translated_seed_verifytext">누락 된 시드 단어를 입력하여 백업을 확인하십시오.</string>
<string name="translated_seed_restore">백업에서 복원</string>
<string name="translated_seed_restoretext">24 개의 시드 단어를 모두 순서대로 입력해야합니다.</string>
<string name="translated_seed_reminder">당신의 자금이 위험에 처해 있습니다</string>
<string name="translated_seed_remindertext">Zcash에서 당신은 은행이라는 것을 기억하십시오. 시드 문구가있는 사람은 누구나 지갑에 액세스 할 수 있습니다.</string>
<string name="translated_seed_remindertext2">다른 사람이 대신 백업 할 수 없으므로 즉시 백업해야합니다.</string>
<string name="translated_seed_accepted">시드 수락, 지금 스캔!</string>
<string name="translated_seed_birthdaytext">지갑에 대한 거래를 위해 블록 체인을 스캔하고 있습니다. 이 지갑의 생일 날짜를 제공 할 수 있다면 동기화 속도를 높일 수 있습니다.</string>
<string name="translated_seed_enterdate">날짜 입력</string>
<string name="translated_seed_birthday">생일</string>
<string name="translated_button_dontknow">몰라</string>
<string name="translated_button_backup">백업</string>
<string name="translated_button_skip">일단은 스킵</string>
<string name="translated_balance_screen">밸런스 화면</string>
<string name="translated_balance_amounttosend">보낼 금액 입력</string>
<string name="translated_balance_expecting">기대 XX ZEC</string>
<string name="translated_balance_available">사용 가능</string>
<string name="translated_balance_nofunds">사용할 수있는 자금 없습니다</string>
<string name="translated_balance_syncing">동기화</string>
<string name="translated_balance_scanning">스캐닝</string>
<string name="translated_button_sendamount">보내는 금액</string>
<string name="translated_button_wallethistory">지갑 내역</string>
<string name="translated_address_screen">당신의 주소</string>
<string name="translated_address_shielded">당신의 쉴드된 주소</string>
<string name="translated_address_transparent">당신의 투명 주소</string>
<string name="translated_feedback_addresscopied">주소 복사</string>
<string name="translated_button_copytoclipboard">클립 보드에 복사</string>
<string name="translated_profile_screen">쉴드된 사용자</string>
<string name="translated_button_feedback">피드백 보내기</string>
<string name="translated_button_backup_wallet">지갑 백업</string>
<string name="translated_button_applicationlogs">애플리케이션 로그보기</string>
<string name="translated_button_close">닫기</string>
<string name="translated_send_screen">보내는 화면</string>
<string name="translated_button_send">보내기</string>
<string name="translated_send_fromshielded">쉴드된 지갑에서</string>
<string name="translated_send_fromtransparent">투명한 지갑에서</string>
<string name="translated_send_fromboth">두 지갑에서</string>
<string name="translated_label_to"></string>
<string name="translated_feedback_default">유효한 zcash 주소를 입력하십시오</string>
<string name="translated_feedback_shieldedaddress">유효한 쉴드된 주소입니다.</string>
<string name="translated_feedback_transparentaddress">유효한 투명 주소입니다.</string>
<string name="translated_feedback_sameaddress">경고, 이것은 귀하의 주소 인 것 같습니다!</string>
<string name="translated_feedback_invalidaddress">경고,이 주소는 유효하지 않습니다!</string>
<string name="translated_label_memo">메모</string>
<string name="translated_label_charactercount">문자</string>
<string name="translated_label_replyto">회신 포함</string>
<string name="translated_send_onclipboard">클립 보드에서</string>
<string name="translated_send_lastused">마지막 사용</string>
<string name="translated_send_unknown">알 수 없는</string>
<string name="translated_send_scanqr">수신자 주소 스캔</string>
<string name="translated_send_securityauth">보내기 인증</string>
<string name="translated_send_securityauth2">XX ZEC를 보낼 것인지 확인하십시오.</string>
<string name="translated_send_sending">XX ZEC를 보내기</string>
<string name="translated_send_cancelled">취소되었습니다</string>
<string name="translated_send_sent">보냈습니다!</string>
<string name="translated_button_done"></string>
<string name="translated_button_seedetails">세부 사항보기</string>
<string name="translated_button_back">돌아가기</string>
<string name="translated_screen_wallethistory">지갑 내역</string>
</resources>
<!-- ISSUES:
- button_backup isn't unique
-->

View File

@ -0,0 +1,16 @@
<resources>
<string name="custom_translation_decimal_key">,</string>
<string name="custom_translation_decimal_separator">,</string>
<string name="custom_translation_format_date_time_brief">d/M HH:mma</string>
<!-- Manual -->
<string name="custom_translation_verify">Проверьте</string>
<string name="custom_translation_birthday">Создано: %1$,d</string>
<string name="custom_translation_expecting">Ожидание</string>
<string name="custom_translation_syncing">Синхронизация…%1$d%%</string>
<string name="custom_translation_scanning">Сканирование…%1$d%%</string>
<string name="custom_translation_send_confirm">Пожалуйста подтвердите, что хотите отправить</string>
<string name="custom_translation_chars">символы</string>
<string name="custom_translation_sending">Отправка</string>
</resources>

View File

@ -0,0 +1,69 @@
<resources>
<string name="translated_app_name">Кошелек ECC</string>
<string name="translated_loadingtext">Загрузка</string>
<string name="translated_welcometext">Добро пожаловать!</string>
<string name="translated_fiattext">В валюте</string>
<string name="translated_title_backupseed">Ваша фраза восстановления</string>
<string name="translated_copy_backupseed">Мы рекомендуем использовать резервную копию на бумаге и хранилище паролей.</string>
<string name="translated_feedback_walletbackupstatus">Необходимо создать резервную копию вашего кошелька.</string>
<string name="translated_seed_verify">Проверьте свою резервную копию.</string>
<string name="translated_seed_verifytext">Пожалуйста, заполните отсутствующие слова фразы восстановления, чтобы проверить свою резервную копию.</string>
<string name="translated_seed_restore">Восстановление из резервной копии</string>
<string name="translated_seed_restoretext">Вам необходимо будет ввести 24 слова фразы восстановления по порядку.</string>
<string name="translated_seed_reminder">Существует риск потерять средства!</string>
<string name="translated_seed_remindertext">Помните, что с Zcash Вы сами находитесь в роли банка. Любой, кто владеет фразой восстановления, получает полный доступ к кошельку.</string>
<string name="translated_seed_remindertext2">Вам следует незамедлительно создать резервную копию, так она позволит восстановить доступ к Вашему кошельку.</string>
<string name="translated_seed_accepted">Фраза восстановления принята, идёт сканирование блокчейна!</string>
<string name="translated_seed_birthdaytext">Мы сканируем блокчейн на предмет транзакций, которые относятся к вашему кошельку. Если вы можете указать дату создания данного кошелька, процесс ускорится.</string>
<string name="translated_seed_enterdate">Введите дату</string>
<string name="translated_seed_birthday">Кошелёк создан</string>
<string name="translated_button_dontknow">Я не знаю</string>
<string name="translated_button_backup">Резервная копия</string>
<string name="translated_button_skip">Пропустить</string>
<string name="translated_balance_screen">Экран баланса</string>
<string name="translated_balance_amounttosend">Введите сумму для отправки</string>
<string name="translated_balance_expecting">Ожидание XX ZEC</string>
<string name="translated_balance_available">Доступно</string>
<string name="translated_balance_nofunds">Нет доступных средств</string>
<string name="translated_balance_syncing">Синхронизация ... %</string>
<string name="translated_balance_scanning">Сканирование ... %</string>
<string name="translated_button_sendamount">Отправить сумму</string>
<string name="translated_button_wallethistory">История транзакций</string>
<string name="translated_address_screen">Ваш адрес</string>
<string name="translated_address_shielded">Ваш защищённый адрес</string>
<string name="translated_address_transparent">Ваш прозрачный адрес</string>
<string name="translated_feedback_addresscopied">Адрес скопирован!</string>
<string name="translated_button_copytoclipboard">Скопировать в буфер обмена</string>
<string name="translated_profile_screen">Защищенный пользователь</string>
<string name="translated_button_feedback">Отправить отзыв</string>
<string name="translated_button_backup_wallet">Резервная копия кошелька</string>
<string name="translated_button_applicationlogs">Просмотреть журнал операций приложения</string>
<string name="translated_button_close">Закрыть</string>
<string name="translated_send_screen">Отправить экран</string>
<string name="translated_button_send">Отправить</string>
<string name="translated_send_fromshielded">с Вашего кошелька</string>
<string name="translated_send_fromtransparent">с Вашего прозрачного кошелька</string>
<string name="translated_send_fromboth">с двух кошельков</string>
<string name="translated_label_to">на</string>
<string name="translated_feedback_default">Введите действующий Zcash-адрес </string>
<string name="translated_feedback_shieldedaddress">Это действующий защищённый адрес</string>
<string name="translated_feedback_transparentaddress">Это действующий прозрачный адрес</string>
<string name="translated_feedback_sameaddress">Внимание, похоже, это ваш адрес!</string>
<string name="translated_feedback_invalidaddress">Внимание, этот адрес недействителен!</string>
<string name="translated_label_memo">Заметка</string>
<string name="translated_label_charactercount">символы (символы)</string>
<string name="translated_label_replyto">добавить "ответ на"</string>
<string name="translated_send_onclipboard">В буфере обмена</string>
<string name="translated_send_lastused">Последний используемый</string>
<string name="translated_send_unknown">Неизвестно</string>
<string name="translated_send_scanqr">Сканировать адрес получателя</string>
<string name="translated_send_securityauth">Подтвердите, чтобы отправить</string>
<string name="translated_send_securityauth2">Пожалуйста подтвердите, что хотите отправить XX ZEC на адрес</string>
<string name="translated_send_sending">Отправка XX ZEC на</string>
<string name="translated_send_cancelled">Отменено</string>
<string name="translated_send_sent">Отправлено!</string>
<string name="translated_button_done">Готово</string>
<string name="translated_button_seedetails">Смотреть подробности</string>
<string name="translated_button_back">Вернуться назад</string>
<string name="translated_screen_wallethistory">История транзакций</string>
</resources>

View File

@ -0,0 +1,16 @@
<resources>
<string name="custom_translation_decimal_key">,</string>
<string name="custom_translation_decimal_separator">,</string>
<string name="custom_translation_format_date_time_brief">d/M HH:mma</string>
<!-- Manual -->
<string name="custom_translation_verify">验证</string>
<string name="custom_translation_birthday">创建日期: %1$,d</string>
<string name="custom_translation_expecting">预计为</string>
<string name="custom_translation_syncing">正在同步…%1$d%%</string>
<string name="custom_translation_scanning">正在扫描…%1$d%%</string>
<string name="custom_translation_send_confirm">请确认,您现在要发送</string>
<string name="custom_translation_chars">字符</string>
<string name="custom_translation_sending">正在发送</string>
</resources>

View File

@ -0,0 +1,69 @@
<resources>
<string name="translated_app_name">ECC 钱包</string>
<string name="translated_loadingtext">准备工作</string>
<string name="translated_welcometext">欢迎使用 ECC 钱包!</string>
<string name="translated_fiattext">法币</string>
<string name="translated_title_backupseed">您的备份种子(助记词)</string>
<string name="translated_copy_backupseed">我们建议您使用纸笔抄录备份种子,并锁到带密码的保险箱里。</string>
<string name="translated_feedback_walletbackupstatus">您的钱包需要备份。</string>
<string name="translated_seed_verify">需要验证您的备份。</string>
<string name="translated_seed_verifytext">请填入缺失的词语,以验证您的备份无误。</string>
<string name="translated_seed_restore">从备份助记词中恢复钱包</string>
<string name="translated_seed_restoretext">您需要按照顺序输入 24 个助记词。</string>
<string name="translated_seed_reminder">您的资金处在安全风险中!</string>
<string name="translated_seed_remindertext">请记住,使用 Zcash 的时候,没有其他人在托管您的资金,您 是自己的银行。使用资金的唯一凭证就是助记词。如有他人知道了您的助记词,就有了转移您资金的所有权限。</string>
<string name="translated_seed_remindertext2">请立即备份,如果您遗失了自己的备份,没有任何人能帮您恢复钱包、找回资金。</string>
<string name="translated_seed_accepted">已接受助记词,正在扫描您的帐户余额。</string>
<string name="translated_seed_birthdaytext">软件正在扫描区块链以发现与您的钱包有关的交易。如果您能提供这个钱包的创建时间,可以加快扫描速度。</string>
<string name="translated_seed_enterdate">输入日期</string>
<string name="translated_seed_birthday">创建日期</string>
<string name="translated_button_dontknow">我不知道 / 我忘了</string>
<string name="translated_button_backup">备份</string>
<string name="translated_button_skip">跳过</string>
<string name="translated_balance_screen">余额扫描</string>
<string name="translated_balance_amounttosend">输入您想发送的数额</string>
<string name="translated_balance_expecting">预计为 XX ZEC</string>
<string name="translated_balance_available">可用</string>
<string name="translated_balance_nofunds">暂无可用的款项</string>
<string name="translated_balance_syncing">正在同步 ...%</string>
<string name="translated_balance_scanning">正在扫描 ...%</string>
<string name="translated_button_sendamount">发送数额</string>
<string name="translated_button_wallethistory">钱包历史</string>
<string name="translated_address_screen">您的地址</string>
<string name="translated_address_shielded">您的隐蔽地址</string>
<string name="translated_address_transparent">您的透明地址</string>
<string name="translated_feedback_addresscopied">已复制地址</string>
<string name="translated_button_copytoclipboard">已复制到剪贴板</string>
<string name="translated_profile_screen">隐蔽用户</string>
<string name="translated_button_feedback">发送反馈</string>
<string name="translated_button_backup_wallet">备份钱包</string>
<string name="translated_button_applicationlogs">查看应用日志</string>
<string name="translated_button_close">关闭</string>
<string name="translated_send_screen">发送资金功能</string>
<string name="translated_button_send">发送</string>
<string name="translated_send_fromshielded">从您的隐蔽钱包发送</string>
<string name="translated_send_fromtransparent">从您的透明钱包发送</string>
<string name="translated_send_fromboth">同时从您的隐蔽钱包和透明钱包拼凑数额</string>
<string name="translated_label_to"></string>
<string name="translated_feedback_default">请输入一个有效的 Zcash 地址</string>
<string name="translated_feedback_shieldedaddress">这是一个有效的隐蔽地址</string>
<string name="translated_feedback_transparentaddress">这是一个有效的透明地址</string>
<string name="translated_feedback_sameaddress">警告,该地址似乎是您自己的地址!</string>
<string name="translated_feedback_invalidaddress">警告,该地址不是一个有效地址!</string>
<string name="translated_label_memo">备注</string>
<string name="translated_label_charactercount">字符</string>
<string name="translated_label_replyto"></string>
<string name="translated_send_onclipboard">在剪贴板内</string>
<string name="translated_send_lastused">上一次使用</string>
<string name="translated_send_unknown">未知</string>
<string name="translated_send_scanqr">扫描接收人地址</string>
<string name="translated_send_securityauth">授权发送</string>
<string name="translated_send_securityauth2">请确认,您现在要发送 XX ZEC 发送给 </string>
<string name="translated_send_sending">正在发送 XX ZEC 给</string>
<string name="translated_send_cancelled">已取消</string>
<string name="translated_send_sent">已发送!</string>
<string name="translated_button_done">完成</string>
<string name="translated_button_seedetails">查看细节</string>
<string name="translated_button_back">返回</string>
<string name="translated_screen_wallethistory">您的钱包历史</string>
</resources>

View File

@ -57,6 +57,9 @@
<!-- every color here should be a reference to a palette color
but have a more useful name for use in code -->
<color name="app_icon_background_0">#FF3F3F4F</color>
<color name="app_icon_background_1">#FF000000</color>
<color name="app_icon_foreground">#FFB900</color>
<color name="background_banner">@color/zcashBlack_dark</color>
<color name="background_banner_stroke">#282828</color>
<color name="scan_overlay_background">@color/zcashBlack_87</color>

View File

@ -0,0 +1,19 @@
<resources>
<string name="custom_translation_decimal_separator">.</string>
<string name="custom_translation_decimal_key">.</string>
<string name="custom_translation_format_date_time_brief">M/d h:mma</string>
<!-- Manual -->
<string name="custom_translation_verify">Verify</string>
<string name="custom_translation_birthday">Birthday Height: %1$,d</string>
<string name="custom_translation_expecting">Expecting</string>
<string name="custom_translation_syncing">Downloading…%1$d%%</string>
<string name="custom_translation_scanning">Scanning…%1$d%%</string>
<string name="custom_translation_send_confirm">Please confirm that you want to send</string>
<string name="custom_translation_from_your">"from your "</string>
<string name="custom_translation_wallet">" wallet"</string>
<string name="custom_translation_chars">chars</string>
<string name="custom_translation_sending">Sending</string>
</resources>

View File

@ -1,71 +1,266 @@
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Common -->
<string name="done">Done</string>
<string name="done" tools:ignore="MissingTranslation">@string/translated_button_done</string>
<string name="cancel">Cancel</string>
<string name="unknown">Unknown</string>
<string name="unknown" tools:ignore="MissingTranslation">@string/translated_send_unknown</string>
<string name="blank"></string>
<!-- Home -->
<string name="home_history_button_text">View History</string>
<string name="home_title">Enter an amount to send</string>
<!-- Transaction details -->
<string name="transaction_title">Transaction Details</string>
<string name="transaction_block_height_prefix">"from block "</string>
<string name="transaction_with_memo">with a memo</string>
<string name="transaction_prefix_from">reply-to</string>
<string name="transaction_prefix_to">to</string>
<!-- -->
<!-- SCREENS -->
<!-- -->
<!-- Send Flow -->
<string name="send_hint_input_zcash_address">Enter a shielded Zcash address</string>
<string name="send_hint_input_zcash_amount">Enter an amount to send</string>
<string name="send_memo_excluded_message">Your transaction is shielded and your address is not available to the recipient</string>
<string name="send_memo_included_message">Your transaction is shielded but your address will be sent to the recipient via the memo</string>
<string name="send_pending_button_text">Cancel</string>
<string name="send_failed_button_text">Retry</string>
<string name="send_complete_button_text">See Details</string>
<string name="send_banner_address_user">Your shielded address</string>
<string name="send_banner_address_unknown">@string/unknown</string>
<!-- Screen: Backup -->
<string name="backup_button_done" tools:ignore="MissingTranslation">@string/translated_button_done</string>
<string name="backup_button_primary">@string/custom_translation_verify</string>
<string name="backup_format_birthday_height">@string/custom_translation_birthday</string>
<string name="backup_instruction_store_words">Store these backup words securely.</string>
<string name="backup_slogan">empowering\neveryone\nwith\neconomic\nfreedom</string>
<string name="backup_verification_not_implemented">Backup verification coming soon!</string>
<!-- Address -->
<string name="address_label_shielded">Your Shielded Address</string>
<string name="scan_address_title">Scan Recipient Address</string>
<string name="receive_address_title">Receive Funds</string>
<!-- Feedback -->
<string name="feedback_question_1">Any details you\'d like to share?</string>
<string name="feedback_question_2">Was your balance clear?</string>
<string name="feedback_question_3">What feature would you like to see next?</string>
<!-- Screen: Feedback -->
<string name="feedback_hint_1">My experience was . . .</string>
<string name="feedback_hint_2">My balance was . . .</string>
<string name="feedback_hint_3">I\'d like . . .</string>
<string name="feedback_question_1">Any details you\'d like to share?</string>
<string name="feedback_question_2">Was your balance clear?</string>
<string name="feedback_question_3">What feature would you like to see next?</string>
<string name="feedback_thanks">Thanks for the feedback!</string>
<!-- Profile -->
<string name="profile_shielded_user">Shielded User</string>
<string name="profile_send_feedback">Send Feedback</string>
<string name="profile_backup_wallet">Backup Wallet</string>
<string name="profile_see_application_logs">See Application Logs</string>
<string name="profile_ecc_wallet">ECC Wallet</string>
<!-- Screen: History -->
<string name="history_address_label">Shielded address:</string>
<string name="history_balance_available" tools:ignore="MissingTranslation">@string/translated_balance_available</string>
<string name="history_balance_updating">Updating</string>
<string name="history_empty_text">No history yet.</string>
<string name="history_header_transactions" tools:ignore="MissingTranslation">@string/translated_screen_wallethistory</string>
<string name="history_instruction_enter_amount" tools:ignore="MissingTranslation">@string/translated_balance_amounttosend</string>
<!-- Screen: Home -->
<string name="home_balance_available" tools:ignore="MissingTranslation">@string/translated_balance_available</string>
<string name="home_balance_updating">Updating</string>
<string name="home_banner_expecting">@string/custom_translation_expecting</string>
<string name="home_button_send_disconnected">Reconnecting . . .</string>
<string name="home_button_send_downloading">@string/custom_translation_syncing</string>
<string name="home_button_send_has_funds" tools:ignore="MissingTranslation">@string/translated_button_sendamount</string>
<string name="home_button_send_idle">IDLE</string>
<string name="home_button_send_no_funds" tools:ignore="MissingTranslation">@string/translated_balance_nofunds</string>
<string name="home_button_send_scanning">@string/custom_translation_scanning</string>
<string name="home_button_send_updating">Updating</string>
<string name="home_button_send_validating">Validating . . .</string>
<string name="home_dialog_no_balance_button_positive">View Address</string>
<string name="home_dialog_no_balance_message">To make full use of this wallet, deposit funds to your address.</string>
<string name="home_dialog_no_balance_title" tools:ignore="MissingTranslation">@string/translated_balance_nofunds</string>
<string name="home_history_button_text" tools:ignore="MissingTranslation">@string/translated_button_wallethistory</string>
<string name="home_instruction_enter_amount" tools:ignore="MissingTranslation">@string/translated_balance_amounttosend</string>
<string name="home_instruction_fund_now">Fund Now</string>
<string name="home_no_balance">@string/translated_balance_nofunds</string>
<string name="home_title" tools:ignore="MissingTranslation">@string/translated_balance_amounttosend</string>
<!-- Screen: Landing -->
<string name="landing_backup_skipped_message_1">Are you sure? Without a backup, funds can be lost FOREVER!</string>
<string name="landing_backup_skipped_message_2">You can\'t backup later. You\'re probably going to lose your funds!</string>
<string name="landing_button_backup_skipped_1">Later</string>
<string name="landing_button_backup_skipped_2">I\'ve been warned</string>
<string name="landing_button_primary">New</string>
<string name="landing_button_primary_create_success" tools:ignore="MissingTranslation">@string/translated_button_backup</string>
<string name="landing_button_progress_create">Creating</string>
<string name="landing_button_secondary">Restore</string>
<string name="landing_button_secondary_create_success" tools:ignore="MissingTranslation">@string/translated_button_skip</string>
<string name="landing_button_secondary_import_success" tools:ignore="MissingTranslation">@string/translated_button_skip</string>
<string name="landing_create_success_message">Wallet created! Congratulations!</string>
<string name="landing_import_success_message">Wallet imported! Congratulations!</string>
<string name="landing_import_success_primary_button" tools:ignore="MissingTranslation">@string/translated_button_backup</string>
<string name="landing_title">Welcome to the ECC Wallet!</string>
<!-- Screen: Profile -->
<string name="profile_app_version">v1.0.0-alpha05</string>
<string name="profile_backup_wallet" tools:ignore="MissingTranslation">@string/translated_button_backup_wallet</string>
<string name="profile_ecc_wallet" tools:ignore="MissingTranslation">@string/translated_app_name</string>
<string name="profile_see_application_logs" tools:ignore="MissingTranslation">@string/translated_button_applicationlogs</string>
<string name="profile_send_feedback" tools:ignore="MissingTranslation">@string/translated_button_feedback</string>
<string name="profile_share_log_title">Share Log Files</string>
<string name="profile_shielded_user" tools:ignore="MissingTranslation">@string/translated_profile_screen</string>
<!-- Settings-->
<!-- Screen: Receive -->
<string name="receive_address_label_shielded" tools:ignore="MissingTranslation">@string/translated_address_shielded</string>
<string name="receive_address_title">Receive Funds</string>
<string name="scan_address_title" tools:ignore="MissingTranslation">@string/translated_send_scanqr</string>
<!-- Screen: Scan -->
<string name="scan_invalid_address">"Invalid Zcash %1$s address:\n%2$s"</string>
<!-- Screen: Send Flow -->
<string name="send_address_hint" tools:ignore="MissingTranslation">@string/translated_label_to</string>
<string name="send_banner_address_unknown" tools:ignore="MissingTranslation">@string/translated_send_unknown</string>
<string name="send_banner_address_user" tools:ignore="MissingTranslation">@string/translated_address_shielded</string>
<string name="send_button_primary" tools:ignore="MissingTranslation">@string/translated_button_send</string>
<string name="send_checkbox_include_address" tools:ignore="MissingTranslation">@string/translated_label_replyto</string>
<string name="send_complete_button_text" tools:ignore="MissingTranslation">@string/translated_button_seedetails</string>
<string name="send_confirmation_prompt">@string/custom_translation_send_confirm</string>
<string name="send_failed_button_text">Retry</string>
<string name="send_final_button_primary_back" tools:ignore="MissingTranslation">@string/translated_button_back</string>
<string name="send_final_button_primary_cancel">Cancel</string>
<string name="send_final_button_primary_details" tools:ignore="MissingTranslation">@string/translated_button_seedetails</string>
<string name="send_final_button_primary_failed">Failed.</string>
<string name="send_final_button_primary_retry">Retry</string>
<string name="send_final_button_primary_sent" tools:ignore="MissingTranslation">@string/translated_send_sent</string>
<string name="send_final_error_encoding">The transaction could not be encoded.</string>
<string name="send_final_error_submitting">Unable to submit transaction to the network.</string>
<string name="send_final_result_cancelled" tools:ignore="MissingTranslation">@string/translated_send_cancelled</string>
<string name="send_final_sending">@string/custom_translation_sending</string>
<string name="send_final_to" tools:ignore="MissingTranslation">@string/translated_label_to</string>
<string name="send_fund_source_highlight">shielded</string>
<string name="send_fund_source_prefix">@string/custom_translation_from_your</string>
<string name="send_fund_source_suffix">@string/custom_translation_wallet</string>
<string name="send_hint_input_zcash_address">Enter a shielded Zcash address</string>
<string name="send_hint_input_zcash_amount" tools:ignore="MissingTranslation">@string/translated_balance_amounttosend</string>
<string name="send_history_clipboard" tools:ignore="MissingTranslation">@string/translated_send_onclipboard</string>
<string name="send_history_last" tools:ignore="MissingTranslation">@string/translated_send_lastused</string>
<string name="send_history_last_and_clipboard">"Last Used and On Clipboard"</string>
<string name="send_memo_chars_abbreviation">@string/custom_translation_chars</string>
<string name="send_memo_excluded_message">Your transaction is shielded and your address is not available to the recipient</string>
<string name="send_memo_hint">Memo</string>
<string name="send_memo_included_message">Your transaction is shielded but your address will be sent to the recipient via the memo</string>
<string name="send_pending_button_text">Cancel</string>
<string name="send_validation_address_invalid">This address appears to be invalid</string>
<string name="send_validation_address_self" tools:ignore="MissingTranslation">@string/translated_feedback_sameaddress</string>
<string name="send_validation_address_valid_taddr" tools:ignore="MissingTranslation">@string/translated_feedback_transparentaddress</string>
<string name="send_validation_address_valid_zaddr" tools:ignore="MissingTranslation">@string/translated_feedback_shieldedaddress</string>
<string name="send_validation_error_address_invalid" tools:ignore="MissingTranslation">@string/translated_feedback_default</string>
<string name="send_validation_error_amount_minimum">Please go back and enter at least 1 Zatoshi.</string>
<string name="send_validation_error_dust">Insufficient funds to cover miner\'s fee.</string>
<string name="send_validation_error_memo_length">Memo must be less than %1$d in length.</string>
<string name="send_validation_error_no_available_funds" tools:ignore="MissingTranslation">@string/translated_balance_nofunds</string>
<string name="send_validation_error_too_much">Please go back and enter no more than %1$s ZEC.</string>
<string name="send_validation_error_unknown_funds">Available funds not found. Please try again in a moment.</string>
<!-- Screen: Settings-->
<string name="settings_buttons_restore">Restore Defaults</string>
<string name="settings_change_lightwalletd_server">Change Lightwalletd Server</string>
<string name="settings_host_helper_text">Enter a valid host name or IP address</string>
<string name="settings_port_helper_text">Enter a valid port number</string>
<string name="settings_reset">Reset</string>
<string name="settings_server_address">Host</string>
<string name="settings_server_port">Port</string>
<string name="settings_toast_change_server_failure">Failed to change server!</string>
<string name="settings_toast_change_server_success">Successfully changed server!</string>
<string name="settings_update">Update</string>
<string name="settings_reset">Reset</string>
<string name="settings_port_helper_text">Enter a valid port number</string>
<string name="settings_host_helper_text">Enter a valid host name or IP address</string>
<!-- Screen: Transaction details -->
<string name="transaction_address_paid_you">paid you</string>
<string name="transaction_address_you_paid">You paid</string>
<string name="transaction_block_height_prefix">"from block "</string>
<string name="transaction_confirmation_count_unavailable">Confirmation count temporarily unavailable</string>
<string name="transaction_details_button_explore">Explore</string>
<string name="transaction_details_total">Total Spent</string>
<string name="transaction_instruction_tap">tap to view</string>
<string name="transaction_prefix_from">reply-to</string>
<string name="transaction_prefix_to" tools:ignore="MissingTranslation">@string/translated_label_to</string>
<string name="transaction_received">Received</string>
<string name="transaction_status_confirmed">Confirmed</string>
<string name="transaction_status_confirming">of 10 confirmations</string>
<string name="transaction_status_expired">Expired</string>
<string name="transaction_status_pending">Pending confirmation</string>
<string name="transaction_status_sent">Sent</string>
<string name="transaction_story_from_shielded" tools:ignore="MissingTranslation">@string/translated_send_fromshielded</string>
<string name="transaction_story_inbound">You Received</string>
<string name="transaction_story_inbound_total">Total Received</string>
<string name="transaction_story_network_fee">+%1$s network fee</string>
<string name="transaction_story_outbound">You Sent</string>
<string name="transaction_story_outbound_total">Total Sent</string>
<string name="transaction_story_to_shielded">to your shielded wallet</string>
<string name="transaction_timestamp_unavailable">Details Unavailable</string>
<string name="transaction_title">Transaction Details</string>
<string name="transaction_with_memo">with a memo</string>
<!-- -->
<!-- Content Descriptions -->
<!-- -->
<string name="content_description_backup_zcash_logo">Zcash Logo</string>
<string name="content_description_history_back">back</string>
<string name="content_description_history_copy">copy</string>
<string name="content_description_home_icon_profile">Profile</string>
<string name="content_description_home_icon_scan">Scan</string>
<string name="content_description_profile_back">back</string>
<string name="content_description_profile_copy">copy</string>
<string name="content_description_profile_settings">settings</string>
<string name="content_description_profile_zebra">zebra</string>
<string name="content_description_receive_back" tools:ignore="MissingTranslation">@string/translated_button_back</string>
<string name="content_description_receive_qr_background">QR background</string>
<string name="content_description_receive_qr_code">QR code</string>
<string name="content_description_receive_qr_logo">QR logo</string>
<string name="content_description_scan_back">back</string>
<string name="content_description_scan_frame">scan frame</string>
<string name="content_description_send_arrow">down arrow</string>
<string name="content_description_send_back">back</string>
<string name="content_description_send_scan_qr">scan QR code</string>
<string name="content_description_send_selected">selected</string>
<string name="content_description_send_shield">shield</string>
<string name="content_description_send_final_back">back</string>
<string name="content_description_settings_back">back</string>
<string name="content_description_transaction_shield">Shield</string>
<string name="content_description_transaction_details_back">back</string>
<string name="content_description_transaction_details_direction">direction</string>
<string name="content_description_transaction_details_memo">memo</string>
<string name="content_description_transaction_details_memo_icon">memo icon</string>
<!-- -->
<!-- Dialogs -->
<string name="dialog_not_again">Don\'t show me again</string>
<string name="dialog_first_use_view_tx_title">Potential Privacy Risk</string>
<string name="dialog_first_use_view_tx_positive">View Tx</string>
<string name="dialog_first_use_view_tx_negative">Cancel</string>
<string name="dialog_first_use_view_tx_message">Are you sure you\'d like to leave the app? This could reduce privacy, if you do not trust the destination.</string>
<!-- -->
<string name="dialog_error_change_server_button_positive">Ok</string>
<string name="dialog_error_change_server_title">Failed to Change Server</string>
<string name="dialog_error_critical_processing_button_negative">Exit</string>
<string name="dialog_error_critical_processing_button_positive">Retry</string>
<string name="dialog_error_critical_processing_message">Critical error while processing blocks!</string>
<string name="dialog_error_critical_processing_title">Processor Error</string>
<string name="dialog_error_invalid_seed_phrase_button_positive">Retry</string>
<string name="dialog_error_invalid_seed_phrase_message">That seed phrase appears to be invalid! Please double-check it and try again.\n\n%1$s</string>
<string name="dialog_error_invalid_seed_phrase_title">Oops! Invalid Seed Phrase</string>
<string name="dialog_error_processor_critical_button_negative">Exit</string>
<string name="dialog_error_processor_critical_button_positive">Retry</string>
<string name="dialog_error_processor_critical_message">Critical error while processing blocks!</string>
<string name="dialog_error_processor_critical_title">Processor Error</string>
<string name="dialog_error_scan_button_negative">Ignore</string>
<string name="dialog_error_scan_button_positive">Retry</string>
<string name="dialog_error_scan_failure_button_negative">Ignore</string>
<string name="dialog_error_scan_failure_button_positive">Retry</string>
<string name="dialog_error_scan_failure_title">Scan Failure</string>
<string name="dialog_error_scan_title">Scan Failure</string>
<string name="dialog_error_uninitialized_button_negative">Clear Data</string>
<string name="dialog_error_uninitialized_button_positive">Exit</string>
<string name="dialog_error_uninitialized_message">This wallet has not been initialized correctly! Perhaps an error occurred during install.\n\nThis can be fixed with a reset. First, locate your backup seed phrase, then CLEAR DATA and reimport it.</string>
<string name="dialog_error_uninitialized_title">Wallet Improperly Initialized</string>
<string name="dialog_first_use_view_tx_message">Are you sure you\'d like to leave the app? This could reduce privacy, if you do not trust the destination.</string>
<string name="dialog_first_use_view_tx_negative">Cancel</string>
<string name="dialog_first_use_view_tx_positive">View Tx</string>
<string name="dialog_first_use_view_tx_title">Potential Privacy Risk</string>
<string name="dialog_modify_server_button_negative">Cancel</string>
<string name="dialog_modify_server_button_positive">Update</string>
<string name="dialog_modify_server_message">WARNING: Entering an invalid or untrusted server might result in misconfiguration or loss of funds!</string>
<string name="dialog_modify_server_title">Modify Lightwalletd Server?</string>
<string name="dialog_not_again">Don\'t show me again</string>
<string name="dialog_nuke_wallet_button_negative">Erase Wallet</string>
<string name="dialog_nuke_wallet_button_positive">Cancel</string>
<string name="dialog_nuke_wallet_message">WARNING: Potential Loss of Funds\n\nClearing all wallet data and can result in a loss of funds, if you cannot locate your correct seed phrase.\n\nPlease confirm that you have your 24-word seed phrase available before proceeding.</string>
<string name="dialog_nuke_wallet_title">Nuke Wallet?</string>
<!-- -->
<!-- Misc -->
<string name="app_name">ECC Wallet</string>
<!-- -->
<string name="app_name" tools:ignore="MissingTranslation">@string/translated_app_name</string>
<string name="biometric_prompt_title" tools:ignore="MissingTranslation">@string/translated_send_securityauth</string>
<string name="camera_permission_denied">Well, this is awkward. You denied permission for the camera.</string>
<string name="error_launch_url">Failed to open browser.</string>
<string name="key_backspace"><![CDATA[<]]></string>
<string name="key_decimal">.</string>
<!-- <string name="key_decimal">@string/custom_translation_decimal_key</string>-->
<string name="mixpanel_project">a178e1ef062133fc121079cb12fa43c7</string>
<string name="format_date_time">@string/custom_translation_format_date_time_brief</string>
</resources>

View File

@ -0,0 +1,74 @@
<resources>
<string name="translated_app_name">ECC Wallet</string>
<string name="translated_loadingtext">Loading</string>
<string name="translated_welcometext">Welcome</string>
<string name="translated_fiattext">Fiat</string>
<string name="translated_title_backupseed">Your backup seed</string>
<string name="translated_copy_backupseed">We recommend a paper backup and a password vault.</string>
<string name="translated_feedback_walletbackupstatus">Your wallet needs to be backed up.</string>
<string name="translated_seed_verify">Verify your backup.</string>
<string name="translated_seed_verifytext">Please fill out the missing seed words to verify your backup.</string>
<string name="translated_seed_restore">Restoring from a Backup</string>
<string name="translated_seed_restoretext">You will need to enter all 24 seed words in order.</string>
<string name="translated_seed_reminder">Your funds are at risk!</string>
<string name="translated_seed_remindertext">Remember, with Zcash YOU are the bank. Anyone with your seed phrase has access to your wallet.</string>
<string name="translated_seed_remindertext2">You should back this up immediately as no one else can restore it for you.</string>
<string name="translated_seed_accepted">Seed accepted, scanning!</string>
<string name="translated_seed_birthdaytext">We are scanning the blockchain for transactions pertaining to your wallet. If you can provide a birthday date for this wallet we can speed it up.</string>
<string name="translated_seed_enterdate">Enter Date</string>
<string name="translated_seed_birthday">Birthday</string>
<string name="translated_button_dontknow">I don\'t know</string>
<string name="translated_button_backup">Backup</string>
<string name="translated_button_skip">Skip for now</string>
<string name="translated_balance_screen">Balance Screen</string>
<string name="translated_balance_amounttosend">Enter an amount to send</string>
<string name="translated_balance_expecting">expecting XX ZEC</string>
<string name="translated_balance_available">Available</string>
<string name="translated_balance_nofunds">No funds available</string>
<string name="translated_balance_syncing">Syncing … %</string>
<string name="translated_balance_scanning">Scanning … %</string>
<string name="translated_button_sendamount">Send Amount</string>
<string name="translated_button_wallethistory">Wallet History</string>
<string name="translated_address_screen">Your Address</string>
<string name="translated_address_shielded">Your Shielded Address</string>
<string name="translated_address_transparent">Your Transparent Address</string>
<string name="translated_feedback_addresscopied">Address Copied!</string>
<string name="translated_button_copytoclipboard">Copy to clipboard</string>
<string name="translated_profile_screen">Shielded User</string>
<string name="translated_button_feedback">Send Feedback</string>
<string name="translated_button_backup_wallet">Backup Wallet</string>
<string name="translated_button_applicationlogs">See Application Logs</string>
<string name="translated_button_close">Close</string>
<string name="translated_send_screen">Send Screen</string>
<string name="translated_button_send">Send</string>
<string name="translated_send_fromshielded">from your shielded wallet</string>
<string name="translated_send_fromtransparent">from your transparent wallet</string>
<string name="translated_send_fromboth">from both wallets</string>
<string name="translated_label_to">To</string>
<string name="translated_feedback_default">Enter a valid Zcash address</string>
<string name="translated_feedback_shieldedaddress">This is a valid shielded address</string>
<string name="translated_feedback_transparentaddress">This is a valid transparent address</string>
<string name="translated_feedback_sameaddress">Warning, this appears to be your address!</string>
<string name="translated_feedback_invalidaddress">Warning, this address is not valid!</string>
<string name="translated_label_memo">Memo</string>
<string name="translated_label_charactercount">chars (characters)</string>
<string name="translated_label_replyto">include reply-to</string>
<string name="translated_send_onclipboard">On Clipboard</string>
<string name="translated_send_lastused">Last Used</string>
<string name="translated_send_unknown">Unknown</string>
<string name="translated_send_scanqr">Scan Recipient Address</string>
<string name="translated_send_securityauth">Authenticate to send</string>
<string name="translated_send_securityauth2">Please confirm you want to send XX ZEC to</string>
<string name="translated_send_sending">Sending XX ZEC to</string>
<string name="translated_send_cancelled">Cancelled</string>
<string name="translated_send_sent">Sent!</string>
<string name="translated_button_done">Done</string>
<string name="translated_button_seedetails">See Details</string>
<string name="translated_button_back">Go Back</string>
<string name="translated_screen_wallethistory">Your Wallet History</string>
</resources>
<!-- ISSUES:
- button_backup isn't unique
-->

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="app_icon_background_0">#FFB900</color>
<color name="app_icon_background_1">#664B00</color>
<color name="app_icon_foreground">#FF000000</color>
</resources>

View File

@ -0,0 +1,3 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name" tools:ignore="MissingTranslation">ECC (QA)</string>
</resources>

View File

@ -1,11 +1,10 @@
package cash.z.ecc.android
import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.*
import cash.z.ecc.android.ui.send.SendViewModel
import cash.z.ecc.android.sdk.entity.*
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.verifyZeroInteractions
import com.nhaarman.mockitokotlin2.whenever
import com.nhaarman.mockitokotlin2.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.test.setMain
@ -54,7 +53,7 @@ class SendViewModelTest {
@Test
fun testUpdateMetrics_creating() {
// doNothing().whenever(sendViewModel).report(any())
doNothing().whenever(sendViewModel).report(any())
assertEquals(true, creatingTx.isCreating())
sendViewModel.updateMetrics(creatingTx)

View File

@ -3,12 +3,16 @@ package cash.z.ecc.android
object Deps {
// For use in the top-level build.gradle which gives an error when provided
// `Deps.Kotlin.version` directly
const val kotlinVersion = "1.4.10"
const val kotlinVersion = "1.4.10"
const val compileSdkVersion = 29
const val buildToolsVersion = "29.0.2"
const val minSdkVersion = 21
const val targetSdkVersion = 29
const val versionName = "1.0.0-alpha37"
const val versionCode = 1_00_00_037 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
const val packageName = "cash.z.ecc.android"
const val compileSdkVersion = 29
const val buildToolsVersion = "29.0.2"
const val minSdkVersion = 21
const val targetSdkVersion = 29
object AndroidX {
const val ANNOTATION = "androidx.annotation:annotation:1.1.0"
@ -34,7 +38,7 @@ object Deps {
val LIFECYCLE_RUNTIME_KTX = "androidx.lifecycle:lifecycle-runtime-ktx:$version"
val LIFECYCLE_EXTENSIONS = "androidx.lifecycle:lifecycle-extensions:$version"
}
object Navigation : Version("2.2.0") {
object Navigation : Version("2.3.0") {
val FRAGMENT_KTX = "androidx.navigation:navigation-fragment-ktx:$version"
val UI_KTX = "androidx.navigation:navigation-ui-ktx:$version"
}
@ -80,25 +84,26 @@ object Deps {
}
object Zcash {
const val ANDROID_WALLET_PLUGINS = "cash.z.ecc.android:zcash-android-wallet-plugins:1.0.0"
const val KOTLIN_BIP39 = "cash.z.ecc.android:kotlin-bip39:1.0.0-beta09"
object Sdk : Version("1.1.0-beta07") {
const val KOTLIN_BIP39 = "cash.z.ecc.android:kotlin-bip39:1.0.1"
object Sdk : Version("1.1.0-beta08") {
val MAINNET = "cash.z.ecc.android:zcash-android-sdk-mainnet:$version"
val TESTNET = "cash.z.ecc.android:sdk-testnet:$version"
val TESTNET = "cash.z.ecc.android:zcash-android-sdk-testnet:$version"
}
}
object Misc {
const val LOTTIE = "com.airbnb.android:lottie:3.1.0"
const val CHIPS = "com.github.gmale:chips-input-layout:2.3.1"
object Plugins {
const val SECURE_STORAGE = "de.adorsys.android:securestoragelibrary:1.2.2"
const val SECURE_STORAGE = "com.github.gmale:secure-storage-android:0.0.3"//"de.adorsys.android:securestoragelibrary:1.2.2"
const val QR_SCANNER = "com.google.zxing:core:3.2.1"
}
}
object Test {
const val JUNIT = "junit:junit:4.12"
const val MOKITO = "junit:junit:4.12"
const val COROUTINES_TEST = "junit:junit:4.12"
const val MOKITO = "org.mockito:mockito-android:3.5.10"
const val MOKITO_KOTLIN = "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
const val COROUTINES_TEST = "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.9"
object Android {
const val JUNIT = "androidx.test.ext:junit:1.1.1"
const val ESPRESSO = "androidx.test.espresso:espresso-core:3.2.0"

View File

@ -1,6 +1,7 @@
package cash.z.ecc.android.lockbox
import android.content.Context
import android.provider.Settings
import cash.z.android.plugin.LockBoxPlugin
import de.adorsys.android.securestoragelibrary.SecurePreferences
import java.nio.ByteBuffer
@ -11,60 +12,72 @@ import javax.inject.Inject
class LockBox @Inject constructor(private val appContext: Context) : LockBoxPlugin {
private val maxLength: Int = 200
private val maxLength: Int = 50
override fun setBoolean(key: String, value: Boolean) {
SecurePreferences.setValue(appContext, key, value)
setChunkedString(key, value.toString())
}
override fun getBoolean(key: String): Boolean {
return SecurePreferences.getBooleanValue(appContext, key, false)
return getChunkedString(key)?.toBoolean() ?: false
}
override fun setBytes(key: String, value: ByteArray) {
// using hex here because this library doesn't really work well for byte arrays
// but hopefully we can code to arrays and then change the underlying library, later
setValue(key, value.toHex())
setChunkedString(key, value.toHex())
}
override fun getBytes(key: String): ByteArray? {
return getValue(key)?.fromHex()
return getChunkedString(key)?.fromHex()
}
override fun setCharsUtf8(key: String, value: CharArray) {
// Using string here because this library doesn't work well for char arrays
// but hopefully we can code to arrays and then change the underlying library, later
setValue(key, String(value))
setChunkedString(key, String(value))
}
override fun getCharsUtf8(key: String): CharArray? {
return getValue(key)?.toCharArray()
return getChunkedString(key)?.toCharArray()
}
fun delete(key: String) {
return SecurePreferences.removeValue(appContext, key)
}
fun clear() {
SecurePreferences.clearAllValues(appContext)
}
inline operator fun <reified T> set(key: String, value: T) {
when (T::class.java) {
Boolean::class.java, Double::class.java, Float::class.java, Integer::class.java, Long::class.java, String::class.java -> setValue(key, value.toString())
else -> throw UnsupportedOperationException("Lockbox does not yet support ${T::class.java.simpleName} objects but it can be added")
when (T::class) {
Boolean::class -> setBoolean(key, value as Boolean)
ByteArray::class -> setBytes(key, value as ByteArray)
CharArray::class -> setCharsUtf8(key, value as CharArray)
Double::class, Float::class, Integer::class, Long::class, String::class -> setChunkedString(key, value.toString())
else -> throw UnsupportedOperationException("Lockbox does not yet support setting ${T::class.java.simpleName} objects but it can easily be added.")
}
}
inline operator fun <reified T> get(key: String): T? = when (T::class.java) {
Boolean::class.java -> (getCharsUtf8(key)?.let { String(it).toIntOrNull() }) as T
Double::class.java -> (getCharsUtf8(key)?.let { String(it).toDoubleOrNull() }) as T
Float::class.java -> (getCharsUtf8(key)?.let { String(it).toFloatOrNull() }) as T
Integer::class.java -> (getCharsUtf8(key)?.let { String(it).toIntOrNull() }) as T
Long::class.java -> (getCharsUtf8(key)?.let { String(it).toLongOrNull() }) as T
String::class.java -> (getCharsUtf8(key)?.let { String(it) }) as T
else -> throw UnsupportedOperationException("Lockbox does not yet support ${T::class.java.simpleName} objects but it can be added")
}
inline operator fun <reified T> get(key: String): T? = when (T::class) {
Boolean::class -> getBoolean(key)
ByteArray::class -> getBytes(key)
CharArray::class -> getCharsUtf8(key)
Double::class -> getChunkedString(key)?.let { it.toDoubleOrNull() }
Float::class -> getChunkedString(key)?.let { it.toFloatOrNull() }
Integer::class -> getChunkedString(key)?.let { it.toIntOrNull() }
Long::class -> getChunkedString(key)?.let { it.toLongOrNull() }
String::class -> getChunkedString(key)
else -> throw UnsupportedOperationException("Lockbox does not yet support getting ${T::class.simpleName} objects but it can easily be added")
} as T
/**
* Splits a string value into smaller pieces so as not to exceed the limit on the length of
* String that can be stored.
*/
fun setValue(key: String, value: String) {
fun setChunkedString(key: String, value: String) {
if (value.length > maxLength) {
SecurePreferences.setValue(appContext, key, value.chunked(maxLength).toSet())
SecurePreferences.setValue(appContext, key, value.chunked(maxLength))
} else {
SecurePreferences.setValue(appContext, key, value)
}
@ -77,9 +90,9 @@ class LockBox @Inject constructor(private val appContext: Context) : LockBoxPlug
*
* @return the key if found and null otherwise.
*/
private fun getValue(key: String): String? {
fun getChunkedString(key: String): String? {
return SecurePreferences.getStringValue(appContext, key, null)
?: SecurePreferences.getStringSetValue(appContext, key, setOf()).let { result ->
?: SecurePreferences.getStringListValue(appContext, key, listOf()).let { result ->
if (result.size == 0) null else result.joinToString("")
}
}

View File

@ -1,3 +1,2 @@
<resources>
<string name="app_name">QRecycler</string>
</resources>