Merge pull request #96 from zcash/release/internal-20200325

Internal ECC release 2020-03-25
This commit is contained in:
Kevin Gorham 2020-03-27 16:58:02 -04:00 committed by GitHub
commit 337a361ef1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 575 additions and 111 deletions

View File

@ -11,7 +11,7 @@ apply plugin: 'com.google.firebase.firebase-perf'
archivesBaseName = 'zcash-android-wallet' archivesBaseName = 'zcash-android-wallet'
group = 'cash.z.ecc.android' group = 'cash.z.ecc.android'
version = '1.0.0-alpha23' version = '1.0.0-alpha25'
android { android {
compileSdkVersion Deps.compileSdkVersion compileSdkVersion Deps.compileSdkVersion
@ -21,7 +21,7 @@ android {
applicationId 'cash.z.ecc.android' applicationId 'cash.z.ecc.android'
minSdkVersion Deps.minSdkVersion minSdkVersion Deps.minSdkVersion
targetSdkVersion Deps.targetSdkVersion targetSdkVersion Deps.targetSdkVersion
versionCode = 1_00_00_023 versionCode = 1_00_00_025
// 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. // 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" versionName = "$version"
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View File

@ -17,7 +17,7 @@ class SynchronizerModule {
@Provides @Provides
@SynchronizerScope @SynchronizerScope
fun provideSynchronizer(appContext: Context, initializer: Initializer): Synchronizer { fun provideSynchronizer(appContext: Context, initializer: Initializer): Synchronizer {
return Synchronizer(appContext, initializer) return Synchronizer(initializer)
} }
} }

View File

@ -5,7 +5,7 @@ import android.text.Spanned
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import androidx.core.text.toSpannable import androidx.core.text.toSpannable
fun String.toColoredSpan(colorResId: Int, coloredPortion: String): Spannable { fun CharSequence.toColoredSpan(colorResId: Int, coloredPortion: String): Spannable {
return toSpannable().apply { return toSpannable().apply {
val start = this@toColoredSpan.indexOf(coloredPortion) val start = this@toColoredSpan.indexOf(coloredPortion)
setSpan(ForegroundColorSpan(colorResId.toAppColor()), start, start + coloredPortion.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) setSpan(ForegroundColorSpan(colorResId.toAppColor()), start, start + coloredPortion.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

View File

@ -22,6 +22,10 @@ fun View.disabledIf(isDisabled: Boolean) {
isEnabled = !isDisabled isEnabled = !isDisabled
} }
fun View.transparentIf(isTransparent: Boolean) {
alpha = if (isTransparent) 0.0f else 1.0f
}
fun View.onClickNavTo(navResId: Int, block: (() -> Any) = {}) { fun View.onClickNavTo(navResId: Int, block: (() -> Any) = {}) {
setOnClickListener { setOnClickListener {
block() block()

View File

@ -43,6 +43,12 @@ object Report {
object ImportCompleted : Restore("importcompleted", 50) object ImportCompleted : Restore("importcompleted", 50)
object Success : Restore("success", 100) object Success : Restore("success", 100)
} }
sealed class UserFeedback(stepName: String, step: Int, vararg properties: Pair<String, Any>) : Feedback.Funnel("feedback", stepName, step, *properties) {
object Started : UserFeedback("started", 0)
object Cancelled : UserFeedback("cancelled", 1)
class Submitted(rating: Int, question1: String, question2: String, question3: String) : UserFeedback("submitted", 100, "rating" to rating, "question1" to question1, "question2" to question2, "question3" to question3)
}
} }
object Error { object Error {
@ -84,6 +90,7 @@ object Report {
DETAIL("wallet.detail"), DETAIL("wallet.detail"),
LANDING, LANDING,
PROFILE, PROFILE,
FEEDBACK,
RECEIVE, RECEIVE,
RESTORE, RESTORE,
SCAN, SCAN,
@ -120,6 +127,8 @@ object Report {
PROFILE_VIEW_USER_LOGS("profile.view.user.logs"), PROFILE_VIEW_USER_LOGS("profile.view.user.logs"),
PROFILE_VIEW_DEV_LOGS("profile.view.dev.logs"), PROFILE_VIEW_DEV_LOGS("profile.view.dev.logs"),
PROFILE_SEND_FEEDBACK("profile.send.feedback"), PROFILE_SEND_FEEDBACK("profile.send.feedback"),
FEEDBACK_CANCEL("feedback.cancel"),
FEEDBACK_SUBMIT("feedback.submit"),
RECEIVE_SCAN("receive.scan"), RECEIVE_SCAN("receive.scan"),
RECEIVE_BACK("receive.back"), RECEIVE_BACK("receive.back"),
RESTORE_DONE("restore.done"), RESTORE_DONE("restore.done"),

View File

@ -50,7 +50,6 @@ import javax.inject.Inject
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@Inject @Inject
lateinit var feedback: Feedback lateinit var feedback: Feedback
@ -324,6 +323,7 @@ class MainActivity : AppCompatActivity() {
showSnackbar("Well, this is awkward. You denied permission for the camera.") showSnackbar("Well, this is awkward. You denied permission for the camera.")
} }
// TODO: clean up this error handling
private var ignoredErrors = 0 private var ignoredErrors = 0
private fun onProcessorError(error: Throwable?): Boolean { private fun onProcessorError(error: Throwable?): Boolean {
var notified = false var notified = false
@ -368,25 +368,25 @@ class MainActivity : AppCompatActivity() {
} }
if (!notified) { if (!notified) {
ignoredErrors++ ignoredErrors++
} if (ignoredErrors >= ZcashSdk.RETRIES) {
if (ignoredErrors >= ZcashSdk.RETRIES) { if (dialog == null) {
if (dialog == null) { notified = true
notified = true runOnUiThread {
runOnUiThread { dialog = MaterialAlertDialogBuilder(this)
dialog = MaterialAlertDialogBuilder(this) .setTitle("Processor Error")
.setTitle("Processor Error") .setMessage(error?.message ?: "Critical error while processing blocks!")
.setMessage(error?.message ?: "Critical error while processing blocks!") .setCancelable(false)
.setCancelable(false) .setPositiveButton("Retry") { d, _ ->
.setPositiveButton("Retry") { d, _ -> d.dismiss()
d.dismiss() dialog = null
dialog = null }
} .setNegativeButton("Exit") { dialog, _ ->
.setNegativeButton("Exit") { dialog, _ -> dialog.dismiss()
dialog.dismiss() throw error
throw error ?: RuntimeException("Critical error while processing blocks and the user chose to exit.")
?: RuntimeException("Critical error while processing blocks and the user chose to exit.") }
} .show()
.show() }
} }
} }
} }

View File

@ -2,11 +2,15 @@ package cash.z.ecc.android.ui.detail
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R import cash.z.ecc.android.R
import cash.z.ecc.android.ext.goneIf import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.toAppColor import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.ui.MainActivity import cash.z.ecc.android.ui.MainActivity
import cash.z.ecc.android.ui.send.SendViewModel
import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIX
import cash.z.ecc.android.ui.util.toUtf8Memo
import cash.z.wallet.sdk.entity.ConfirmedTransaction import cash.z.wallet.sdk.entity.ConfirmedTransaction
import cash.z.wallet.sdk.ext.* import cash.z.wallet.sdk.ext.*
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -21,6 +25,7 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
private val bottomText = itemView.findViewById<TextView>(R.id.text_transaction_bottom) private val bottomText = itemView.findViewById<TextView>(R.id.text_transaction_bottom)
private val shieldIcon = itemView.findViewById<View>(R.id.image_shield) private val shieldIcon = itemView.findViewById<View>(R.id.image_shield)
private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault()) private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault())
private val addressRegex = """zs\d\w{65,}""".toRegex()
fun bindTo(transaction: T?) { fun bindTo(transaction: T?) {
@ -29,13 +34,19 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
var lineTwo: String = "" var lineTwo: String = ""
var amountZec: String = "" var amountZec: String = ""
var amountDisplay: String = "" var amountDisplay: String = ""
var amountColor: Int = 0 var amountColor: Int = R.color.text_light_dimmed
var indicatorBackground: Int = 0 var lineOneColor: Int = R.color.text_light
var lineTwoColor: Int = R.color.text_light_dimmed
var indicatorBackground: Int = R.drawable.background_indicator_unknown
transaction?.apply { transaction?.apply {
itemView.setOnClickListener { itemView.setOnClickListener {
onTransactionClicked(this) onTransactionClicked(this)
} }
itemView.setOnLongClickListener {
onTransactionLongPressed(this)
true
}
amountZec = value.convertZatoshiToZecString() amountZec = value.convertZatoshiToZecString()
// TODO: these might be good extension functions // TODO: these might be good extension functions
val timestamp = formatter.format(blockTimeInSeconds * 1000L) val timestamp = formatter.format(blockTimeInSeconds * 1000L)
@ -45,11 +56,16 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
lineOne = "You paid ${toAddress?.toAbbreviatedAddress()}" lineOne = "You paid ${toAddress?.toAbbreviatedAddress()}"
lineTwo = if (isMined) "Sent $timestamp" else "Pending confirmation" lineTwo = if (isMined) "Sent $timestamp" else "Pending confirmation"
amountDisplay = "- $amountZec" amountDisplay = "- $amountZec"
amountColor = R.color.zcashRed if (isMined) {
indicatorBackground = R.drawable.background_indicator_outbound amountColor = R.color.zcashRed
indicatorBackground = R.drawable.background_indicator_outbound
} else {
lineOneColor = R.color.text_light_dimmed
lineTwoColor = R.color.text_light
}
} }
raw == null || raw?.isEmpty() == true -> { toAddress.isNullOrEmpty() && value > 0L && minedHeight > 0 -> {
lineOne = "Unknown paid you" lineOne = getSender(transaction)
lineTwo = "Received $timestamp" lineTwo = "Received $timestamp"
amountDisplay = "+ $amountZec" amountDisplay = "+ $amountZec"
amountColor = R.color.zcashGreen amountColor = R.color.zcashGreen
@ -58,9 +74,10 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
else -> { else -> {
lineOne = "Unknown" lineOne = "Unknown"
lineTwo = "Unknown" lineTwo = "Unknown"
amountDisplay = "$amountZec"
amountColor = R.color.text_light
} }
} }
// sanitize amount // sanitize amount
if (value < ZcashSdk.MINERS_FEE_ZATOSHI) amountDisplay = "< 0.001" 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 else if (amountZec.length > 10) { // 10 allows 3 digits to the left and 6 to the right of the decimal
@ -73,17 +90,41 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
bottomText.text = lineTwo bottomText.text = lineTwo
amountText.text = amountDisplay amountText.text = amountDisplay
amountText.setTextColor(amountColor.toAppColor()) amountText.setTextColor(amountColor.toAppColor())
topText.setTextColor(lineOneColor.toAppColor())
bottomText.setTextColor(lineTwoColor.toAppColor())
val context = itemView.context val context = itemView.context
indicator.background = context.resources.getDrawable(indicatorBackground) indicator.background = context.resources.getDrawable(indicatorBackground)
shieldIcon.goneIf((transaction?.raw != null || transaction?.expiryHeight != null) && !transaction?.toAddress.isShielded()) shieldIcon.goneIf((transaction?.raw != null || transaction?.expiryHeight != null) && !transaction?.toAddress.isShielded())
} }
private fun getSender(transaction: ConfirmedTransaction): String {
val memo = transaction.memo.toUtf8Memo()
return when {
memo.contains(INCLUDE_MEMO_PREFIX) -> {
val address = memo.split(INCLUDE_MEMO_PREFIX)[1].trim()
"${address.toAbbreviatedAddress()} paid you"
}
memo.contains("eply to:") -> {
val address = memo.split("eply to:")[1].trim()
"${address.toAbbreviatedAddress()} paid you"
}
memo.contains("zs") -> {
val who = extractAddress(memo)?.toAbbreviatedAddress() ?: "Unknown"
"$who paid you"
}
else -> "Unknown paid you"
}
}
private fun extractAddress(memo: String?) =
addressRegex.findAll(memo ?: "").lastOrNull()?.value
private fun onTransactionClicked(transaction: ConfirmedTransaction) { private fun onTransactionClicked(transaction: ConfirmedTransaction) {
val txId = transaction.rawTransactionId.toTxId() val txId = transaction.rawTransactionId.toTxId()
val detailsMessage: String = "Zatoshi amount: ${transaction.value}\n\n" + val detailsMessage: String = "Zatoshi amount: ${transaction.value}\n\n" +
"Transaction: $txId" + "Transaction: $txId" +
"${if (transaction.toAddress != null) "\n\nto: ${transaction.toAddress}" else ""}" + "${if (transaction.toAddress != null) "\n\nTo: ${transaction.toAddress}" else ""}" +
"${if (transaction.memo != null) "\n\nmemo: \n${String(transaction.memo!!, Charset.forName("UTF-8"))}" else ""}" "${if (transaction.memo != null) "\n\nMemo: \n${String(transaction.memo!!, Charset.forName("UTF-8"))}" else ""}"
MaterialAlertDialogBuilder(itemView.context) MaterialAlertDialogBuilder(itemView.context)
.setMessage(detailsMessage) .setMessage(detailsMessage)
@ -98,6 +139,12 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
} }
.show() .show()
} }
private fun onTransactionLongPressed(transaction: ConfirmedTransaction) {
(transaction.toAddress ?: extractAddress(transaction.memo.toUtf8Memo()))?.let {
(itemView.context as MainActivity).copyText(it, "Transaction Address")
}
}
} }
private fun ByteArray.toTxId(): String { private fun ByteArray.toTxId(): String {

View File

@ -66,6 +66,7 @@ class WalletDetailFragment : BaseFragment<FragmentDetailBinding>() {
adapter = TransactionAdapter() adapter = TransactionAdapter()
viewModel.transactions.collectWith(resumedScope) { onTransactionsUpdated(it) } viewModel.transactions.collectWith(resumedScope) { onTransactionsUpdated(it) }
binding.recyclerTransactions.adapter = adapter binding.recyclerTransactions.adapter = adapter
binding.recyclerTransactions.smoothScrollToPosition(0)
} }
private fun onTransactionsUpdated(transactions: PagedList<ConfirmedTransaction>) { private fun onTransactionsUpdated(transactions: PagedList<ConfirmedTransaction>) {

View File

@ -19,7 +19,8 @@ import cash.z.ecc.android.ui.send.SendViewModel
import cash.z.ecc.android.ui.setup.WalletSetupViewModel import cash.z.ecc.android.ui.setup.WalletSetupViewModel
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.NO_SEED import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.NO_SEED
import cash.z.wallet.sdk.Synchronizer import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.Synchronizer.Status.SYNCED import cash.z.wallet.sdk.Synchronizer.Status.*
import cash.z.wallet.sdk.block.CompactBlockProcessor
import cash.z.wallet.sdk.ext.convertZatoshiToZecString import cash.z.wallet.sdk.ext.convertZatoshiToZecString
import cash.z.wallet.sdk.ext.convertZecToZatoshi import cash.z.wallet.sdk.ext.convertZecToZatoshi
import cash.z.wallet.sdk.ext.safelyConvertToBigDecimal import cash.z.wallet.sdk.ext.safelyConvertToBigDecimal
@ -92,7 +93,6 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
buttonNumberPadBack.asKey() buttonNumberPadBack.asKey()
) )
hitAreaReceive.onClickNavTo(R.id.action_nav_home_to_nav_profile) { tapped(HOME_PROFILE) } hitAreaReceive.onClickNavTo(R.id.action_nav_home_to_nav_profile) { tapped(HOME_PROFILE) }
iconDetail.onClickNavTo(R.id.action_nav_home_to_nav_detail) { tapped(HOME_DETAIL) }
textDetail.onClickNavTo(R.id.action_nav_home_to_nav_detail) { tapped(HOME_DETAIL) } textDetail.onClickNavTo(R.id.action_nav_home_to_nav_detail) { tapped(HOME_DETAIL) }
hitAreaScan.setOnClickListener { hitAreaScan.setOnClickListener {
mainActivity?.maybeOpenScan().also { tapped(HOME_SCAN) } mainActivity?.maybeOpenScan().also { tapped(HOME_SCAN) }
@ -195,8 +195,8 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
} }
fun setProgress(uiModel: HomeViewModel.UiModel) { fun setProgress(uiModel: HomeViewModel.UiModel) {
if (!uiModel.processorInfo.hasData) { if (!uiModel.processorInfo.hasData && !uiModel.isDisconnected) {
twig("Warning: ignoring progress update because the processor has not started.") twig("Warning: ignoring progress update because the processor is still starting.")
return return
} }
@ -207,28 +207,21 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
} }
val sendText = when { val sendText = when {
uiModel.status == DISCONNECTED -> "Reconnecting . . ."
uiModel.isSynced -> if (uiModel.hasFunds) "SEND AMOUNT" else "NO FUNDS AVAILABLE" uiModel.isSynced -> if (uiModel.hasFunds) "SEND AMOUNT" else "NO FUNDS AVAILABLE"
uiModel.status == Synchronizer.Status.DISCONNECTED -> "DISCONNECTED" uiModel.status == STOPPED -> "IDLE"
uiModel.status == Synchronizer.Status.STOPPED -> "IDLE"
uiModel.isDownloading -> "Downloading . . . ${snake.downloadProgress}%" uiModel.isDownloading -> "Downloading . . . ${snake.downloadProgress}%"
uiModel.isValidating -> "Validating . . ." uiModel.isValidating -> "Validating . . ."
uiModel.isScanning -> "Scanning . . . ${snake.scanProgress}%" uiModel.isScanning -> "Scanning . . . ${snake.scanProgress}%"
else -> "Updating" else -> "Updating"
} }
// binding.lottieButtonLoading.progress = if (uiModel.isSynced) 1.0f else uiModel.totalProgress * 0.82f // line fully closes at 82% mark
binding.buttonSendAmount.text = sendText binding.buttonSendAmount.text = sendText
// twig("Lottie progress set to ${binding.lottieButtonLoading.progress} (isSynced? ${uiModel.isSynced})")
twig("Send button set to: $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 val resId = if (uiModel.isSynced) R.color.selector_button_text_dark else R.color.selector_button_text_light
binding.buttonSendAmount.setTextColor(resources.getColorStateList(resId)) binding.buttonSendAmount.setTextColor(resources.getColorStateList(resId))
binding.lottieButtonLoading.invisibleIf(uiModel.isDisconnected)
// if (uiModel.status == DISCONNECTED || uiModel.status == STOPPED) {
// binding.buttonSendAmount.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.zcashGray))
// } else {
// binding.buttonSendAmount.backgroundTintList = null
// }
} }
/** /**
@ -243,10 +236,13 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
} }
fun setAvailable(availableBalance: Long = -1L, totalBalance: Long = -1L) { fun setAvailable(availableBalance: Long = -1L, totalBalance: Long = -1L) {
val availableString = if (availableBalance < 0) "Updating" else availableBalance.convertZatoshiToZecString() val missingBalance = availableBalance < 0
val availableString = if (missingBalance) "Updating" else availableBalance.convertZatoshiToZecString()
binding.textBalanceAvailable.text = availableString binding.textBalanceAvailable.text = availableString
binding.textBalanceAvailable.transparentIf(missingBalance)
binding.labelBalance.transparentIf(missingBalance)
binding.textBalanceDescription.apply { binding.textBalanceDescription.apply {
goneIf(availableBalance < 0) goneIf(missingBalance)
text = if (availableBalance != -1L && (availableBalance < totalBalance)) { text = if (availableBalance != -1L && (availableBalance < totalBalance)) {
val change = (totalBalance - availableBalance).convertZatoshiToZecString() val change = (totalBalance - availableBalance).convertZatoshiToZecString()
"(expecting +$change ZEC)".toColoredSpan(R.color.text_light, "+$change") "(expecting +$change ZEC)".toColoredSpan(R.color.text_light, "+$change")
@ -275,12 +271,10 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
private fun onModelUpdated(old: HomeViewModel.UiModel?, new: HomeViewModel.UiModel) { private fun onModelUpdated(old: HomeViewModel.UiModel?, new: HomeViewModel.UiModel) {
logUpdate(old, new) logUpdate(old, new)
if (binding.lottieButtonLoading.visibility != View.VISIBLE) binding.lottieButtonLoading.visibility = View.VISIBLE
uiModel = new uiModel = new
if (old?.pendingSend != new.pendingSend) { if (old?.pendingSend != new.pendingSend) {
setSendAmount(new.pendingSend) setSendAmount(new.pendingSend)
} }
// TODO: handle stopped and disconnected flows
setProgress(uiModel) // TODO: we may not need to separate anymore setProgress(uiModel) // TODO: we may not need to separate anymore
// if (new.status = SYNCING) onSyncing(new) else onSynced(new) // if (new.status = SYNCING) onSyncing(new) else onSynced(new)
if (new.status == SYNCED) onSynced(new) else onSyncing(new) if (new.status == SYNCED) onSynced(new) else onSyncing(new)

View File

@ -21,8 +21,7 @@ class HomeViewModel @Inject constructor() : ViewModel() {
lateinit var uiModels: Flow<UiModel> lateinit var uiModels: Flow<UiModel>
private val _typedChars = ConflatedBroadcastChannel<Char>() lateinit var _typedChars: ConflatedBroadcastChannel<Char>
private val typedChars = _typedChars.asFlow()
var initialized = false var initialized = false
@ -32,12 +31,19 @@ class HomeViewModel @Inject constructor() : ViewModel() {
twig("Warning already initialized HomeViewModel. Ignoring call to initialize.") twig("Warning already initialized HomeViewModel. Ignoring call to initialize.")
return return
} }
if (::_typedChars.isInitialized) {
_typedChars.close()
}
_typedChars = ConflatedBroadcastChannel()
val typedChars = _typedChars.asFlow()
val zec = typedChars.scan("0") { acc, c -> val zec = typedChars.scan("0") { acc, c ->
when { when {
// no-op cases // no-op cases
acc == "0" && c == '0' acc == "0" && c == '0'
|| (c == '<' && acc == "0") || (c == '<' && acc == "0")
|| (c == '.' && acc.contains('.')) -> {twig("triggered: 1 acc: $acc c: $c $typedChars ") || (c == '.' && acc.contains('.')) -> {twig("triggered: 1 acc: $acc c: $c")
acc acc
} }
c == '<' && acc.length <= 1 -> {twig("triggered: 2 $typedChars") c == '<' && acc.length <= 1 -> {twig("triggered: 2 $typedChars")
@ -96,6 +102,7 @@ class HomeViewModel @Inject constructor() : ViewModel() {
val isDownloading = status == DOWNLOADING val isDownloading = status == DOWNLOADING
val isScanning = status == SCANNING val isScanning = status == SCANNING
val isValidating = status == VALIDATING val isValidating = status == VALIDATING
val isDisconnected = status == DISCONNECTED
val downloadProgress: Int get() { val downloadProgress: Int get() {
return processorInfo.run { return processorInfo.run {
if (lastDownloadRange.isEmpty()) { if (lastDownloadRange.isEmpty()) {

View File

@ -7,7 +7,7 @@ import com.airbnb.lottie.LottieAnimationView
class MagicSnakeLoader( class MagicSnakeLoader(
val lottie: LottieAnimationView, val lottie: LottieAnimationView,
private val scanningStartFrame: Int = 100, private val scanningStartFrame: Int = 100,
private val scanningEndFrame: Int = 175, private val scanningEndFrame: Int = 187,
val totalFrames: Int = 200 val totalFrames: Int = 200
) : ValueAnimator.AnimatorUpdateListener { ) : ValueAnimator.AnimatorUpdateListener {
private var isPaused: Boolean = true private var isPaused: Boolean = true

View File

@ -0,0 +1,92 @@
package cash.z.ecc.android.ui.profile
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewTreeObserver
import android.view.WindowManager
import android.widget.Toast
import androidx.core.view.doOnLayout
import cash.z.ecc.android.databinding.FragmentFeedbackBinding
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Funnel.UserFeedback
import cash.z.ecc.android.feedback.Report.Tap.FEEDBACK_CANCEL
import cash.z.ecc.android.feedback.Report.Tap.FEEDBACK_SUBMIT
import cash.z.ecc.android.ui.base.BaseFragment
/**
* Fragment representing the home screen of the app. This is the screen most often seen by the user when launching the
* application.
*/
class FeedbackFragment : BaseFragment<FragmentFeedbackBinding>() {
override val screen = Report.Screen.FEEDBACK
override fun inflate(inflater: LayoutInflater): FragmentFeedbackBinding =
FragmentFeedbackBinding.inflate(inflater)
private lateinit var ratings: Array<View>
// private val padder = ViewTreeObserver.OnGlobalLayoutListener {
// Toast.makeText(mainActivity, "LAYOUT", Toast.LENGTH_SHORT).show()
// }
//
// LifeCycle
//
override fun onResume() {
super.onResume()
// mainActivity!!.window.decorView.viewTreeObserver.addOnGlobalLayoutListener(padder)
// mainActivity!!.findViewById<View>(android.R.id.content).viewTreeObserver.addOnGlobalLayoutListener(padder)
}
override fun onPause() {
super.onPause()
// mainActivity!!.window.decorView.viewTreeObserver.removeOnGlobalLayoutListener(padder)
// mainActivity!!.findViewById<View>(android.R.id.content).viewTreeObserver.removeOnGlobalLayoutListener(padder)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
backButtonHitArea.setOnClickListener(::onFeedbackCancel)
buttonSubmit.setOnClickListener(::onFeedbackSubmit)
ratings = arrayOf(feedbackExp1, feedbackExp2, feedbackExp3, feedbackExp4, feedbackExp5)
ratings.forEach {
it.setOnClickListener(::onRatingClicked)
}
}
}
//
// Private API
//
private fun onFeedbackSubmit(view: View) {
Toast.makeText(mainActivity, "Thanks for the feedback!", Toast.LENGTH_LONG).show()
tapped(FEEDBACK_SUBMIT)
val q1 = binding.inputQuestion1.editText?.text.toString()
val q2 = binding.inputQuestion2.editText?.text.toString()
val q3 = binding.inputQuestion3.editText?.text.toString()
val rating = ratings.indexOfFirst { it.isActivated } + 1
mainActivity?.reportFunnel(UserFeedback.Submitted(rating, q1, q2, q3))
mainActivity?.navController?.navigateUp()
}
private fun onFeedbackCancel(view: View) {
tapped(FEEDBACK_CANCEL)
mainActivity?.reportFunnel(UserFeedback.Cancelled)
mainActivity?.navController?.navigateUp()
}
private fun onRatingClicked(view: View) {
ratings.forEach { it.isActivated = false }
view.isActivated = !view.isActivated
}
}

View File

@ -1,6 +1,5 @@
package cash.z.ecc.android.ui.profile package cash.z.ecc.android.ui.profile
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
@ -17,6 +16,7 @@ import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.ext.onClickNavTo import cash.z.ecc.android.ext.onClickNavTo
import cash.z.ecc.android.feedback.FeedbackFile import cash.z.ecc.android.feedback.FeedbackFile
import cash.z.ecc.android.feedback.Report 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.feedback.Report.Tap.*
import cash.z.ecc.android.ui.base.BaseFragment import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.wallet.sdk.ext.toAbbreviatedAddress import cash.z.wallet.sdk.ext.toAbbreviatedAddress
@ -39,6 +39,11 @@ class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.hitAreaClose.onClickNavBack() { tapped(PROFILE_CLOSE) } binding.hitAreaClose.onClickNavBack() { tapped(PROFILE_CLOSE) }
binding.buttonBackup.onClickNavTo(R.id.action_nav_profile_to_nav_backup) { tapped(PROFILE_BACKUP) } binding.buttonBackup.onClickNavTo(R.id.action_nav_profile_to_nav_backup) { tapped(PROFILE_BACKUP) }
binding.buttonFeedback.onClickNavTo(R.id.action_nav_profile_to_nav_feedback) {
tapped(PROFILE_SEND_FEEDBACK)
mainActivity?.reportFunnel(UserFeedback.Started)
Unit
}
binding.textVersion.text = BuildConfig.VERSION_NAME binding.textVersion.text = BuildConfig.VERSION_NAME
onClick(binding.buttonLogs) { onClick(binding.buttonLogs) {
tapped(PROFILE_VIEW_USER_LOGS) tapped(PROFILE_VIEW_USER_LOGS)
@ -49,10 +54,6 @@ class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
onViewDevLogs() onViewDevLogs()
true true
} }
onClick(binding.buttonFeedback) {
tapped(PROFILE_SEND_FEEDBACK)
onSendFeedback()
}
} }
override fun onResume() { override fun onResume() {
@ -95,10 +96,6 @@ class ProfileFragment : BaseFragment<FragmentProfileBinding>() {
startActivity(Intent.createChooser(intent, "Share Log File")) startActivity(Intent.createChooser(intent, "Share Log File"))
} }
private fun onSendFeedback() {
mainActivity?.showSnackbar("Feedback feature coming soon!")
}
private fun userLogFile(): File? { private fun userLogFile(): File? {
return mainActivity?.feedbackCoordinator?.findObserver<FeedbackFile>()?.file return mainActivity?.feedbackCoordinator?.findObserver<FeedbackFile>()?.file
} }

View File

@ -54,7 +54,7 @@ class QrAnalyzer(val scanCallback: (qrContent: String, image: ImageProxy) -> Uni
private fun onImageScan(result: List<FirebaseVisionBarcode>, image: ImageProxy) { private fun onImageScan(result: List<FirebaseVisionBarcode>, image: ImageProxy) {
result.firstOrNull()?.rawValue?.let { result.firstOrNull()?.rawValue?.let {
scanCallback(it, image) scanCallback(it, image)
} ?: image.close() } ?: runCatching { image.close() }
} }
private fun onImageScanFailure(e: Exception) { private fun onImageScanFailure(e: Exception) {

View File

@ -150,8 +150,8 @@ class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
private fun onBalanceUpdated(balance: WalletBalance) { private fun onBalanceUpdated(balance: WalletBalance) {
binding.textLayoutAmount.helperText = binding.textLayoutAmount.helperText =
"You have ${balance.availableZatoshi.convertZatoshiToZecString(8)} available" "You have ${balance.availableZatoshi.coerceAtLeast(0L).convertZatoshiToZecString(8)} available"
maxZatoshi = balance.availableZatoshi - ZcashSdk.MINERS_FEE_ZATOSHI maxZatoshi = (balance.availableZatoshi - ZcashSdk.MINERS_FEE_ZATOSHI).coerceAtLeast(0L)
} }
override fun onPrimaryClipChanged() { override fun onPrimaryClipChanged() {

View File

@ -13,6 +13,7 @@ import cash.z.ecc.android.feedback.Report.MetricType
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.lockbox.LockBox
import cash.z.ecc.android.ui.setup.WalletSetupViewModel import cash.z.ecc.android.ui.setup.WalletSetupViewModel
import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIX
import cash.z.wallet.sdk.Initializer import cash.z.wallet.sdk.Initializer
import cash.z.wallet.sdk.Synchronizer import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.entity.* import cash.z.wallet.sdk.entity.*
@ -51,7 +52,7 @@ class SendViewModel @Inject constructor() : ViewModel() {
var includeFromAddress: Boolean = false var includeFromAddress: Boolean = false
set(value) { set(value) {
require(!value || (value && !fromAddress.isNullOrEmpty())) { require(!value || (value && !fromAddress.isNullOrEmpty())) {
"Error: from address was empty while attempting to include it in the memo. Verify" + "Error: fromAddress was empty while attempting to include it in the memo. Verify" +
" that initFromAddress() has previously been called on this viewmodel." " that initFromAddress() has previously been called on this viewmodel."
} }
field = value field = value
@ -60,7 +61,7 @@ class SendViewModel @Inject constructor() : ViewModel() {
fun send(): Flow<PendingTransaction> { fun send(): Flow<PendingTransaction> {
funnel(SendSelected) funnel(SendSelected)
val memoToSend = if (includeFromAddress) "$memo\nsent from\n$fromAddress" else memo val memoToSend = createMemoToSend()
val keys = initializer.deriveSpendingKeys( val keys = initializer.deriveSpendingKeys(
lockBox.getBytes(WalletSetupViewModel.LockBoxKey.SEED)!! lockBox.getBytes(WalletSetupViewModel.LockBoxKey.SEED)!!
) )
@ -76,6 +77,8 @@ class SendViewModel @Inject constructor() : ViewModel() {
} }
} }
fun createMemoToSend() = if (includeFromAddress) "$memo\n$INCLUDE_MEMO_PREFIX\n$fromAddress" else memo
private fun reportIssues(memoToSend: String) { private fun reportIssues(memoToSend: String) {
if (toAddress == fromAddress) feedback.report(Issue.SelfSend) if (toAddress == fromAddress) feedback.report(Issue.SelfSend)
when { when {
@ -98,13 +101,16 @@ class SendViewModel @Inject constructor() : ViewModel() {
when { when {
synchronizer.validateAddress(toAddress).isNotValid -> { synchronizer.validateAddress(toAddress).isNotValid -> {
emit("Please enter a valid address") emit("Please enter a valid address.")
} }
zatoshiAmount < 1 -> { zatoshiAmount < 1 -> {
emit("Too little! Please enter at least 1 Zatoshi.") emit("Please enter at least 1 Zatoshi.")
} }
maxZatoshi != null && zatoshiAmount > maxZatoshi -> { maxZatoshi != null && zatoshiAmount > maxZatoshi -> {
emit( "Too much! Please enter no more than ${maxZatoshi.convertZatoshiToZecString(8)}") emit( "Please enter no more than ${maxZatoshi.convertZatoshiToZecString(8)} ZEC.")
}
createMemoToSend().length > ZcashSdk.MAX_MEMO_SIZE -> {
emit( "Memo must be less than ${ZcashSdk.MAX_MEMO_SIZE} in length.")
} }
else -> emit(null) else -> emit(null)
} }
@ -191,7 +197,6 @@ class SendViewModel @Inject constructor() : ViewModel() {
private fun Keyed<String>.toMetricIdFor(id: Long): String = "$id.$key" private fun Keyed<String>.toMetricIdFor(id: Long): String = "$id.$key"
private fun String.toRelatedMetricId(): String = "$this.related" private fun String.toRelatedMetricId(): String = "$this.related"
private fun String.toTxId(): Long = split('.').first().toLong() private fun String.toTxId(): Long = split('.').first().toLong()
} }

View File

@ -0,0 +1,28 @@
package cash.z.ecc.android.ui.util
import java.nio.charset.StandardCharsets
const val INCLUDE_MEMO_PREFIX = "sent from"
inline fun ByteArray?.toUtf8Memo(): String {
// TODO: make this more official but for now, this will do
return if (this == null || this[0] >= 0xF5) "" else try {
String(this, StandardCharsets.UTF_8).trim('\u0000')
} catch (t: Throwable) {
"unable to parse memo"
}
}
/*
if self.0[0] < 0xF5 {
// Check if it is valid UTF8
Some(str::from_utf8(&self.0).map(|memo| {
// Drop trailing zeroes
memo.trim_end_matches(char::from(0)).to_owned()
}))
} else {
None
}
*/

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_activated="false" android:color="@color/text_light"/>
<item android:state_activated="true" android:color="@color/colorPrimary" />
</selector>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<gradient
android:angle="270"
android:endColor="@color/zcashWhite_12"
android:startColor="@color/zcashWhite_60" />
</shape>

View File

@ -0,0 +1,271 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Back Button -->
<ImageView
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:tint="@color/text_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.05"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.065"
app:srcCompat="@drawable/ic_arrow_back_black_24dp" />
<View
android:id="@+id/back_button_hit_area"
android:layout_width="56dp"
android:layout_height="56dp"
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" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/back_button"
app:layout_constraintStart_toStartOf="parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/content_feedback"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.1" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.9" />
<TextView
android:id="@+id/text_feedback_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Please rank your experience"
android:textColor="@color/text_light"
android:textSize="22sp"
app:layout_constraintStart_toEndOf="@id/guideline_content_start"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text_feedback_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="We improve and iterate with YOUR feedback"
android:textColor="@color/text_light"
android:textSize="12sp"
app:layout_constraintStart_toEndOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/text_feedback_title" />
<View
android:id="@+id/background_buttons"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginTop="16dp"
android:background="@color/zcashBlack_54"
app:layout_constraintTop_toBottomOf="@id/text_feedback_subtitle" />
<TextView
android:id="@+id/feedback_exp_1"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@drawable/background_circle"
android:backgroundTint="@color/selector_feedback_button"
android:gravity="center"
android:text="1"
android:textColor="@color/text_dark"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="@id/background_buttons"
app:layout_constraintEnd_toStartOf="@id/feedback_exp_2"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toTopOf="@id/background_buttons" />
<TextView
android:id="@+id/feedback_exp_2"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@drawable/background_circle"
android:backgroundTint="@color/selector_feedback_button"
android:gravity="center"
android:text="2"
android:textColor="@color/text_dark"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="@id/background_buttons"
app:layout_constraintEnd_toStartOf="@id/feedback_exp_3"
app:layout_constraintStart_toEndOf="@id/feedback_exp_1"
app:layout_constraintTop_toTopOf="@id/background_buttons" />
<TextView
android:id="@+id/feedback_exp_3"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@drawable/background_circle"
android:backgroundTint="@color/selector_feedback_button"
android:gravity="center"
android:text="3"
android:textColor="@color/text_dark"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="@id/background_buttons"
app:layout_constraintEnd_toStartOf="@id/feedback_exp_4"
app:layout_constraintStart_toEndOf="@id/feedback_exp_2"
app:layout_constraintTop_toTopOf="@id/background_buttons" />
<TextView
android:id="@+id/feedback_exp_4"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@drawable/background_circle"
android:backgroundTint="@color/selector_feedback_button"
android:gravity="center"
android:text="4"
android:textColor="@color/text_dark"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="@id/background_buttons"
app:layout_constraintEnd_toStartOf="@id/feedback_exp_5"
app:layout_constraintStart_toEndOf="@id/feedback_exp_3"
app:layout_constraintTop_toTopOf="@id/background_buttons" />
<TextView
android:id="@+id/feedback_exp_5"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@drawable/background_circle"
android:backgroundTint="@color/selector_feedback_button"
android:gravity="center"
android:text="5"
android:textColor="@color/text_dark"
android:textSize="30dp"
app:layout_constraintBottom_toBottomOf="@id/background_buttons"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toEndOf="@id/feedback_exp_4"
app:layout_constraintTop_toTopOf="@id/background_buttons" />
<TextView
android:id="@+id/text_question_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/feedback_question_1"
android:textColor="@color/text_light"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/background_buttons" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_question_1"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:hint="@string/feedback_hint_1"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/text_question_1">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:lines="3" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/text_question_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/feedback_question_2"
android:textColor="@color/text_light"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/input_question_1" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_question_2"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:hint="@string/feedback_hint_2"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/text_question_2">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:lines="3" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/text_question_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/feedback_question_3"
android:textColor="@color/text_light"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/input_question_2" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_question_3"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:hint="@string/feedback_hint_3"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/text_question_3">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:lines="3" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_submit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
style="@style/Zcash.Button"
android:gravity="center"
android:padding="12dp"
android:text="Send Feedback"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="#000000"
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
app:layout_constraintTop_toBottomOf="@id/input_question_3" />
<Space
android:id="@+id/extra_padding_for_scrolling"
android:layout_width="match_parent"
android:layout_height="400dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_submit" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -241,18 +241,6 @@
app:layout_constraintTop_toBottomOf="@id/button_number_pad_9" app:layout_constraintTop_toBottomOf="@id/button_number_pad_9"
app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent" /> app:layout_constraintWidth_percent="@dimen/calculator_button_width_percent" />
<!-- <com.google.android.material.button.MaterialButton-->
<!-- android:id="@+id/button_send"-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="0dp"-->
<!-- style="@style/Zcash.Button"-->
<!-- android:text=""-->
<!-- android:enabled="false"-->
<!-- app:layout_constraintEnd_toEndOf="@id/guide_keys"-->
<!-- app:layout_constraintStart_toStartOf="@id/guide_keys"-->
<!-- app:layout_constraintTop_toTopOf="@id/lottie_button_loading"-->
<!-- app:layout_constraintBottom_toBottomOf="@id/lottie_button_loading"/>-->
<View <View
android:id="@+id/layer_lock" android:id="@+id/layer_lock"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -307,7 +295,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:elevation="6dp" android:elevation="6dp"
android:tint="@color/colorAccent" app:tint="@color/colorAccent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.052" app:layout_constraintHeight_percent="0.052"
@ -333,21 +321,6 @@
app:layout_constraintWidth_percent="0.08" app:layout_constraintWidth_percent="0.08"
app:srcCompat="@drawable/ic_account_circle" /> app:srcCompat="@drawable/ic_account_circle" />
<ImageView
android:id="@+id/icon_detail"
android:layout_width="0dp"
android:layout_height="0dp"
android:elevation="6dp"
android:tint="@color/colorAccent"
app:layout_constraintBottom_toBottomOf="@id/text_detail"
app:layout_constraintEnd_toStartOf="@id/text_detail"
app:layout_constraintHeight_percent="0.044"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/text_detail"
app:layout_constraintWidth_percent="0.0887"
app:srcCompat="@drawable/ic_receipt_24dp" />
<View <View
android:id="@+id/hit_area_scan" android:id="@+id/hit_area_scan"
android:layout_width="68dp" android:layout_width="68dp"
@ -376,13 +349,13 @@
android:padding="12dp" android:padding="12dp"
android:elevation="6dp" android:elevation="6dp"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
android:text="Wallet Details" android:text="Wallet History"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorAccent" android:textColor="@color/colorAccent"
android:tint="@color/colorAccent" android:tint="@color/colorAccent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/icon_detail" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/lottie_button_loading" /> app:layout_constraintTop_toBottomOf="@id/lottie_button_loading" />
<TextView <TextView

View File

@ -223,7 +223,7 @@
android:id="@+id/group_transparent" android:id="@+id/group_transparent"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:visibility="visible" tools:visibility="gone"
app:constraint_referenced_ids="sad_description, sad_icon, sad_title, sad_checkbox" /> app:constraint_referenced_ids="sad_description, sad_icon, sad_title, sad_checkbox" />
<androidx.constraintlayout.widget.Group <androidx.constraintlayout.widget.Group
@ -231,5 +231,5 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:constraint_referenced_ids="clear_memo, background_memo, input_memo, check_include_address, text_info_shielded" app:constraint_referenced_ids="clear_memo, background_memo, input_memo, check_include_address, text_info_shielded"
tools:visibility="gone" /> tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -65,7 +65,6 @@
android:id="@+id/nav_detail" android:id="@+id/nav_detail"
android:name="cash.z.ecc.android.ui.detail.WalletDetailFragment" android:name="cash.z.ecc.android.ui.detail.WalletDetailFragment"
tools:layout="@layout/fragment_detail" /> tools:layout="@layout/fragment_detail" />
<fragment <fragment
android:id="@+id/nav_profile" android:id="@+id/nav_profile"
android:name="cash.z.ecc.android.ui.profile.ProfileFragment" android:name="cash.z.ecc.android.ui.profile.ProfileFragment"
@ -73,7 +72,14 @@
<action <action
android:id="@+id/action_nav_profile_to_nav_backup" android:id="@+id/action_nav_profile_to_nav_backup"
app:destination="@id/nav_backup" /> app:destination="@id/nav_backup" />
<action
android:id="@+id/action_nav_profile_to_nav_feedback"
app:destination="@id/nav_feedback" />
</fragment> </fragment>
<fragment
android:id="@+id/nav_feedback"
android:name="cash.z.ecc.android.ui.profile.FeedbackFragment"
tools:layout="@layout/fragment_feedback" />
<!-- --> <!-- -->
<!-- Send Navigation --> <!-- Send Navigation -->

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<dimen name="buttonCornerRadius">0dp</dimen>
<!-- Floats --> <!-- Floats -->

View File

@ -8,7 +8,15 @@
<!-- Send Flow --> <!-- Send Flow -->
<string name="send_hint_input_zcash_address">Enter a shielded Zcash address</string> <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_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_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_memo_included_message">Your transaction is shielded but your address will be sent to the recipient via the memo</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>
<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>
</resources> </resources>

View File

@ -10,6 +10,7 @@
<item name="shapeAppearanceSmallComponent">@style/Zcash.ShapeAppearance.SmallComponent</item> <item name="shapeAppearanceSmallComponent">@style/Zcash.ShapeAppearance.SmallComponent</item>
<item name="shapeAppearanceMediumComponent">@style/Zcash.ShapeAppearance.MediumComponent</item> <item name="shapeAppearanceMediumComponent">@style/Zcash.ShapeAppearance.MediumComponent</item>
</style> </style>
<style name="ZcashTheme" parent="ZcashBaseTheme"/> <style name="ZcashTheme" parent="ZcashBaseTheme"/>
@ -67,17 +68,22 @@
<!-- Shape Appearances --> <!-- Shape Appearances -->
<!-- General: buttons -->
<style name="Zcash.ShapeAppearance.SmallComponent" parent="ShapeAppearance.MaterialComponents.SmallComponent"> <style name="Zcash.ShapeAppearance.SmallComponent" parent="ShapeAppearance.MaterialComponents.SmallComponent">
<item name="cornerFamily">rounded</item> <item name="cornerFamily">rounded</item>
<item name="cornerSize">20dp</item> <item name="cornerSize">8dp</item>
</style> </style>
<!-- General: dialogs -->
<style name="Zcash.ShapeAppearance.MediumComponent" parent="ShapeAppearance.MaterialComponents.MediumComponent"> <style name="Zcash.ShapeAppearance.MediumComponent" parent="ShapeAppearance.MaterialComponents.MediumComponent">
<item name="cornerFamily">rounded</item> <item name="cornerFamily">rounded</item>
<item name="cornerSize">24dp</item> <item name="cornerSize">12dp</item>
</style> </style>
<!-- Component -->
<style name="Zcash.ShapeAppearance.TextInputLayout" parent="ShapeAppearance.MaterialComponents.SmallComponent"> <style name="Zcash.ShapeAppearance.TextInputLayout" parent="ShapeAppearance.MaterialComponents.SmallComponent">
<item name="cornerFamily">rounded</item> <item name="cornerFamily">rounded</item>
<item name="cornerSize">8dp</item> <item name="cornerSize">0dp</item>
</style> </style>
<!-- Theme Overlays --> <!-- Theme Overlays -->