Merge pull request #191 from zcash/feature/update-send-flow

Feature/update send flow
This commit is contained in:
Kevin Gorham 2020-08-01 05:02:56 -04:00 committed by GitHub
commit 01860b448d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1285 additions and 237 deletions

View File

@ -10,7 +10,7 @@ apply plugin: 'com.google.firebase.crashlytics'
archivesBaseName = 'zcash-android-wallet'
group = 'cash.z.ecc.android'
version = '1.0.0-alpha31'
version = '1.0.0-alpha32'
android {
ndkVersion "21.1.6352462"
@ -21,7 +21,7 @@ android {
applicationId 'cash.z.ecc.android'
minSdkVersion Deps.minSdkVersion
targetSdkVersion Deps.targetSdkVersion
versionCode = 1_00_00_031
versionCode = 1_00_00_032
// 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"
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
@ -109,6 +109,7 @@ dependencies {
// Android
implementation Deps.AndroidX.ANNOTATION
implementation Deps.AndroidX.APPCOMPAT
implementation Deps.AndroidX.BIOMETRICS
implementation Deps.AndroidX.CONSTRAINT_LAYOUT
implementation Deps.AndroidX.CORE_KTX
implementation Deps.AndroidX.FRAGMENT_KTX

View File

@ -6,9 +6,19 @@ import cash.z.ecc.android.ui.MainActivity
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.channelFlow
fun View.gone() = goneIf(true)
fun View.gone() {
visibility = GONE
}
fun View.invisible() = invisibleIf(true)
fun View.invisible() {
visibility = INVISIBLE
}
fun View.visible() {
visibility = VISIBLE
}
// NOTE: avoid `visibleIf` function because the false case is ambiguous: would it be gone or invisible?
fun View.goneIf(isGone: Boolean) {
visibility = if (isGone) GONE else VISIBLE

View File

@ -140,6 +140,7 @@ object Report {
SEND_ADDRESS_MAX("send.address.max"),
SEND_ADDRESS_NEXT("send.address.next"),
SEND_ADDRESS_PASTE("send.address.paste"),
SEND_ADDRESS_REUSE("send.address.reuse"),
SEND_ADDRESS_BACK("send.address.back"),
SEND_ADDRESS_DONE_ADDRESS("send.address.done.address"),
SEND_ADDRESS_DONE_AMOUNT("send.address.done.amount"),
@ -156,6 +157,8 @@ object Report {
SEND_MEMO_CLEAR("send.memo.clear"),
SEND_MEMO_BACK("send.memo.back"),
SEND_SUBMIT("send.submit"),
// General events
COPY_ADDRESS("copy.address");

View File

@ -20,6 +20,8 @@ import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.annotation.IdRes
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricConstants
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment
@ -76,6 +78,12 @@ class MainActivity : AppCompatActivity() {
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
val latestHeight: Int? get() = if (::synchronizerComponent.isInitialized) {
synchronizerComponent.synchronizer().latestHeight
} else {
null
}
override fun onCreate(savedInstanceState: Bundle?) {
component = ZcashWalletApp.component.mainActivitySubcomponent().create(this).also {
it.inject(this)
@ -187,6 +195,43 @@ class MainActivity : AppCompatActivity() {
action?.let { feedback.report(it) }
}
fun authenticate(description: String, block: () -> Unit) {
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
twig("Authentication success")
block()
}
override fun onAuthenticationFailed() {
twig("Authentication failed!!!!")
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
twig("Authenticatiion Error")
when (errorCode) {
BiometricConstants.ERROR_HW_NOT_PRESENT, BiometricConstants.ERROR_HW_UNAVAILABLE,
BiometricConstants.ERROR_NO_BIOMETRICS, BiometricConstants.ERROR_NO_DEVICE_CREDENTIAL -> {
twig("Warning: bypassing authentication because $errString [$errorCode]")
block()
}
else -> {
twig("Warning: failed authentication because $errString [$errorCode]")
}
}
}
}
BiometricPrompt(this, ContextCompat.getMainExecutor(this), callback).apply {
authenticate(
BiometricPrompt.PromptInfo.Builder()
.setTitle("Authenticate to Proceed")
.setConfirmationRequired(false)
.setDescription(description)
.setDeviceCredentialAllowed(true)
.build()
)
}
}
fun playSound(fileName: String) {
mediaPlayer.apply {
if (isPlaying) stop()

View File

@ -7,11 +7,15 @@ import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.R
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.toAppColor
import cash.z.ecc.android.ui.MainActivity
import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIX
import cash.z.ecc.android.ui.util.toUtf8Memo
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.ext.*
import cash.z.ecc.android.sdk.db.entity.PendingTransactionEntity
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.ext.isShielded
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.ui.MainActivity
import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIXES_RECOGNIZED
import cash.z.ecc.android.ui.util.toUtf8Memo
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.launch
import java.nio.charset.Charset
@ -55,6 +59,8 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
!toAddress.isNullOrEmpty() -> {
lineOne = "You paid ${toAddress?.toAbbreviatedAddress()}"
lineTwo = if (isMined) "Sent $timestamp" else "Pending confirmation"
// 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!! < (itemView.context as MainActivity).latestHeight ?: -1)) lineTwo = "Expired"
amountDisplay = "- $amountZec"
if (isMined) {
amountColor = R.color.zcashRed
@ -94,32 +100,42 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
bottomText.setTextColor(lineTwoColor.toAppColor())
val context = itemView.context
indicator.background = context.resources.getDrawable(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())
}
}
private suspend fun getSender(transaction: ConfirmedTransaction): String {
val memo = transaction.memo.toUtf8Memo()
val who = extractValidAddress(memo, INCLUDE_MEMO_PREFIX)
?: extractValidAddress(memo, "sent from:")
?: "Unknown"
val who = extractValidAddress(memo)?.toAbbreviatedAddress() ?: "Unknown"
return "$who paid you"
}
private fun extractAddress(memo: String?) =
addressRegex.findAll(memo ?: "").lastOrNull()?.value
private suspend fun extractValidAddress(memo: String?, delimiter: String): String? {
private suspend fun extractValidAddress(memo: String?): String? {
if (memo == null || memo.length < 25) return null
// note: cannot use substringAfterLast because we need to ignore case
return memo?.lastIndexOf(delimiter, ignoreCase = true)?.let { i ->
memo.substring(i + delimiter.length).trimStart()
}?.validateAddress()
try {
INCLUDE_MEMO_PREFIXES_RECOGNIZED.forEach { prefix ->
memo.lastIndexOf(prefix, ignoreCase = true).takeUnless { it == -1 }?.let { lastIndex ->
memo.substring(lastIndex + prefix.length).trimStart().validateAddress()?.let { address ->
return@extractValidAddress address
}
}
}
} catch(t: Throwable) { }
return null
}
private fun onTransactionClicked(transaction: ConfirmedTransaction) {
val txId = transaction.rawTransactionId.toTxId()
val detailsMessage: String = "Zatoshi amount: ${transaction.value}\n\n" +
"Mined height: ${transaction.minedHeight}\n\n" +
"Transaction: $txId" +
"${if (transaction.toAddress != null) "\n\nTo: ${transaction.toAddress}" else ""}" +
"${if (transaction.memo != null) "\n\nMemo: \n${String(transaction.memo!!, Charset.forName("UTF-8"))}" else ""}"

View File

@ -60,19 +60,39 @@ class WalletDetailFragment : BaseFragment<FragmentDetailBinding>() {
}
private fun initTransactionUI() {
binding.recyclerTransactions.layoutManager =
LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
binding.recyclerTransactions.addItemDecoration(TransactionsFooter(binding.recyclerTransactions.context))
adapter = TransactionAdapter()
binding.recyclerTransactions.apply {
layoutManager =
LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
addItemDecoration(TransactionsFooter(binding.recyclerTransactions.context))
adapter = this@WalletDetailFragment.adapter
scrollToTop()
}
viewModel.transactions.collectWith(resumedScope) { onTransactionsUpdated(it) }
binding.recyclerTransactions.adapter = adapter
binding.recyclerTransactions.smoothScrollToPosition(0)
}
private fun onTransactionsUpdated(transactions: PagedList<ConfirmedTransaction>) {
twig("got a new paged list of transactions")
binding.groupEmptyViews.goneIf(transactions.size > 0)
adapter.submitList(transactions)
transactions.size.let { newCount ->
binding.groupEmptyViews.goneIf(newCount > 0)
val preSize = adapter.itemCount
adapter.submitList(transactions)
// don't rescroll while the user is looking at the list, unless it's initialization
// using 4 here because there might be headers or other things that make 0 a bad pick
// 4 is about how many can fit before scrolling becomes necessary on many screens
if (preSize < 4 && newCount > preSize) {
scrollToTop()
}
}
}
private fun scrollToTop() {
twig("scrolling to the top")
binding.recyclerTransactions.apply {
postDelayed({
smoothScrollToPosition(0)
}, 5L)
}
}
// TODO: maybe implement this for better fade behavior. Or do an actual scroll behavior instead, yeah do that. Or an item decoration.

View File

@ -115,12 +115,14 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
}
if (::uiModel.isInitialized) {
twig("uiModel exists!")
onModelUpdated(null, uiModel)
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)))
}
}
private fun onClearAmount() {
twig("onClearAmount()")
if (::uiModel.isInitialized) {
resumedScope.launch {
binding.textSendAmount.text.apply {
@ -228,6 +230,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
* @param amount the amount to send represented as ZEC, without the dollar sign.
*/
fun setSendAmount(amount: String, updateModel: Boolean = true) {
twig("setSendAmount($amount, $updateModel)")
binding.textSendAmount.text = "\$$amount".toColoredSpan(R.color.text_light_dimmed, "$")
if (updateModel) {
sendViewModel.zatoshiAmount = amount.safelyConvertToBigDecimal().convertZecToZatoshi()

View File

@ -127,14 +127,13 @@ class ScanFragment : BaseFragment<FragmentScanBinding>() {
private fun onQrScanned(qrContent: String, image: ImageProxy) {
resumedScope.launch {
if (viewModel.isNotValid(qrContent)) {
//todo: use the "NETWORK" constant that will be available in the next SDK build
val network = ZcashSdk.DEFAULT_DB_NAME_PREFIX.split("_")[1]
val network = ZcashSdk.NETWORK
binding.textScanError.text = "Invalid Zcash $network address:\n$qrContent"
image.close()
} else { /* continue scanning*/
binding.textScanError.text = ""
sendViewModel.toAddress = qrContent
mainActivity?.safeNavigate(R.id.action_nav_scan_to_nav_send_address)
mainActivity?.safeNavigate(R.id.action_nav_scan_to_nav_send)
}
}
}

View File

@ -106,18 +106,18 @@ class SendAddressFragment : BaseFragment<FragmentSendAddressBinding>(),
private fun onSubmit(unused: EditText? = null) {
sendViewModel.toAddress = binding.inputZcashAddress.text.toString()
binding.inputZcashAmount.convertZecToZatoshi()?.let { sendViewModel.zatoshiAmount = it }
sendViewModel.validate(maxZatoshi).onFirstWith(resumedScope) {
if (it == null) {
sendViewModel.funnel(Send.AddressPageComplete)
mainActivity?.safeNavigate(R.id.action_nav_send_address_to_send_memo)
} else {
resumedScope.launch {
binding.textAddressError.text = it
delay(1500L)
binding.textAddressError.text = ""
}
}
}
// sendViewModel.validate(maxZatoshi).onFirstWith(resumedScope) {
// if (it == null) {
// sendViewModel.funnel(Send.AddressPageComplete)
//// mainActivity?.safeNavigate(R.id.action_nav_send_address_to_send_memo)
// } else {
// resumedScope.launch {
// binding.textAddressError.text = it
// delay(1500L)
// binding.textAddressError.text = ""
// }
// }
// }
}
private fun onMax() {

View File

@ -30,10 +30,10 @@ class SendConfirmFragment : BaseFragment<FragmentSendConfirmBinding>() {
binding.buttonNext.setOnClickListener {
onSend().also { tapped(SEND_CONFIRM_NEXT) }
}
R.id.action_nav_send_confirm_to_nav_send_memo.let {
binding.backButtonHitArea.onClickNavTo(it) { tapped(SEND_CONFIRM_BACK) }
onBackPressNavTo(it) { tapped(SEND_CONFIRM_BACK) }
}
// R.id.action_nav_send_confirm_to_nav_send_memo.let {
// binding.backButtonHitArea.onClickNavTo(it) { tapped(SEND_CONFIRM_BACK) }
// onBackPressNavTo(it) { tapped(SEND_CONFIRM_BACK) }
// }
mainActivity?.lifecycleScope?.launch {
binding.textConfirmation.text =
"Send ${sendViewModel.zatoshiAmount.convertZatoshiToZecString(8)} ZEC to ${sendViewModel?.toAddress.toAbbreviatedAddress()}?"
@ -46,6 +46,6 @@ class SendConfirmFragment : BaseFragment<FragmentSendConfirmBinding>() {
private fun onSend() {
sendViewModel.funnel(Send.ConfirmPageComplete)
mainActivity?.safeNavigate(R.id.action_nav_send_confirm_to_send_final)
// mainActivity?.safeNavigate(R.id.action_nav_send_confirm_to_send_final)
}
}

View File

@ -10,17 +10,15 @@ import cash.z.ecc.android.databinding.FragmentSendFinalBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.*
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.feedback.Report.Tap.SEND_FINAL_CLOSE
import cash.z.ecc.android.feedback.Report.Tap.SEND_FINAL_EXIT
import cash.z.ecc.android.sdk.db.entity.*
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 kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import cash.z.ecc.android.ui.base.BaseFragment
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlin.random.Random
class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
override val screen = Report.Screen.SEND_FINAL
@ -32,21 +30,17 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonNext.setOnClickListener {
onExit().also { tapped(SEND_FINAL_EXIT) }
binding.buttonPrimary.setOnClickListener {
onReturnToSend()
}
binding.buttonRetry.setOnClickListener {
onRetry().also { tapped(SEND_FINAL_RETRY) }
binding.buttonSecondary.setOnClickListener {
onExit().also { tapped(SEND_FINAL_EXIT) }
}
binding.backButtonHitArea.setOnClickListener {
onExit().also { tapped(SEND_FINAL_CLOSE) }
}
binding.textConfirmation.text =
"Sending ${sendViewModel.zatoshiAmount.convertZatoshiToZecString(8)} ZEC to ${sendViewModel.toAddress.toAbbreviatedAddress()}"
sendViewModel.memo.trim().isNotEmpty().let { hasMemo ->
binding.radioIncludeAddress.isChecked = hasMemo
binding.radioIncludeAddress.goneIf(!hasMemo)
}
"Sending ${sendViewModel.zatoshiAmount.convertZatoshiToZecString(8)} ZEC to\n${sendViewModel.toAddress.toAbbreviatedAddress()}"
mainActivity?.preventBackPress(this)
}
@ -55,60 +49,36 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
mainActivity?.apply {
sendViewModel.send().onEach {
onPendingTxUpdated(it)
}.launchIn(mainActivity?.lifecycleScope!!)
}.launchIn(lifecycleScope)
}
}
override fun onResume() {
super.onResume()
flow {
val max = binding.progressHorizontal.max - 1
var progress = 0
while (progress < max) {
emit(progress)
delay(Random.nextLong(1000))
progress++
}
}.onEach {
binding.progressHorizontal.progress = it
}.launchIn(resumedScope)
}
private fun onPendingTxUpdated(tx: PendingTransaction?) {
if (tx == null) return // TODO: maybe log this
private fun onPendingTxUpdated(pendingTransaction: PendingTransaction?) {
try {
if (pendingTransaction != null) sendViewModel.updateMetrics(pendingTransaction)
val id = pendingTransaction?.id ?: -1
var isSending = true
var isFailure = false
var step: Report.Funnel.Send? = null
val message = when {
pendingTransaction == null -> "Transaction not found".also { step = Report.Funnel.Send.ErrorNotFound }
pendingTransaction.isMined() -> "Transaction Mined!\n\nSEND COMPLETE".also { isSending = false; step = Report.Funnel.Send.Mined(pendingTransaction.minedHeight) }
pendingTransaction.isSubmitSuccess() -> "Successfully submitted transaction!\nAwaiting confirmation . . .".also { step = Report.Funnel.Send.Submitted }
pendingTransaction.isFailedEncoding() -> "ERROR: failed to encode transaction! (id: $id)".also { isSending = false; isFailure = true; step = Report.Funnel.Send.ErrorEncoding(pendingTransaction?.errorCode, pendingTransaction?.errorMessage) }
pendingTransaction.isFailedSubmit() -> "ERROR: failed to submit transaction! (id: $id)".also { isSending = false; isFailure = true; step = Report.Funnel.Send.ErrorSubmitting(pendingTransaction?.errorCode, pendingTransaction?.errorMessage) }
pendingTransaction.isCreated() -> "Transaction creation complete!".also { step = Report.Funnel.Send.Created(id) }
pendingTransaction.isCreating() -> "Creating transaction . . .".also { step = Report.Funnel.Send.Creating }
else -> "Transaction updated!".also { twig("Unhandled TX state: $pendingTransaction") }
tx.toUiModel().let { model ->
binding.apply {
backButton.goneIf(!model.showCloseIcon)
backButtonHitArea.goneIf(!model.showCloseIcon)
buttonSecondary.goneIf(!model.showCloseIcon)
textConfirmation.text = model.title
lottieSending.goneIf(!model.showProgress)
if (!model.showProgress) lottieSending.pauseAnimation() else lottieSending.playAnimation()
errorMessage.text = model.errorMessage
buttonPrimary.apply {
text = model.primaryButtonText
setOnClickListener { model.primaryAction() }
}
}
}
sendViewModel.funnel(step)
twig("Pending TX (id: ${pendingTransaction?.id} Updated with message: $message")
binding.textStatus.apply {
text = "$message"
}
binding.backButton.goneIf(!binding.textStatus.text.toString().contains("Awaiting"))
binding.buttonNext.goneIf((pendingTransaction?.isSubmitSuccess() != true) && (pendingTransaction?.isCreated() != true) && !isFailure)
binding.buttonNext.text = if (isSending) "Done" else "Finished"
binding.buttonRetry.goneIf(!isFailure)
binding.progressHorizontal.goneIf(!isSending)
if (pendingTransaction?.isSubmitSuccess() == true) {
// only hold onto the view model if the transaction failed so that the user can retry
if (tx.isSubmitSuccess()) {
sendViewModel.reset()
}
} catch(t: Throwable) {
} catch (t: Throwable) {
val message = "ERROR: error while handling pending transaction update! $t"
twig(message)
mainActivity?.feedback?.report(Report.Error.NonFatal.TxUpdateFailed(t))
@ -117,11 +87,64 @@ class SendFinalFragment : BaseFragment<FragmentSendFinalBinding>() {
}
private fun onExit() {
sendViewModel.reset()
mainActivity?.navController?.popBackStack(R.id.nav_home, false)
}
private fun onRetry() {
mainActivity?.navController?.popBackStack(R.id.nav_send_address, false)
private fun onCancel(tx: PendingTransaction) {
sendViewModel.cancel(tx.id)
}
private fun onReturnToSend() {
mainActivity?.navController?.popBackStack(R.id.nav_send, false)
}
private fun onSeeDetails() {
sendViewModel.reset()
mainActivity?.safeNavigate(R.id.action_nav_send_final_to_nav_detail)
}
private fun PendingTransaction.toUiModel() = UiModel().also { model ->
when {
isCancelled() -> {
model.title = "Cancelled."
model.primaryButtonText = "Go Back"
model.primaryAction = { onReturnToSend() }
}
isSubmitSuccess() -> {
model.title = "SENT!"
model.primaryButtonText = "See 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.primaryAction = { onReturnToSend() }
}
else -> {
model.title = "Sending ${value.convertZatoshiToZecString(8)} ZEC to\n${toAddress.toAbbreviatedAddress()}"
model.showProgress = true
if (isCreating()) {
model.showCloseIcon = false
model.primaryButtonText = "Cancel"
model.primaryAction = { onCancel(this) }
} else {
model.primaryButtonText = "See Details"
model.primaryAction = { onSeeDetails() }
}
}
}
}
// fields are ordered, as they appear, top-to-bottom in the UI because that makes it easier to reason about each screen state
data class UiModel(
var showCloseIcon: Boolean = true,
var title: String = "",
var showProgress: Boolean = false,
var errorMessage: String = "",
var primaryButtonText: String = "See Details",
var primaryAction: () -> Unit = {}
)
}

View File

@ -0,0 +1,309 @@
package cash.z.ecc.android.ui.send
import android.content.ClipboardManager
import android.content.Context
import android.content.res.ColorStateList
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.EditText
import androidx.biometric.BiometricConstants.*
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentSendBinding
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.feedback.Report.Tap.*
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.WalletBalance
import cash.z.ecc.android.sdk.ext.*
import cash.z.ecc.android.sdk.validate.AddressType
import cash.z.ecc.android.ui.base.BaseFragment
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import java.util.concurrent.Executor
class SendFragment : BaseFragment<FragmentSendBinding>(),
ClipboardManager.OnPrimaryClipChangedListener {
override val screen = Report.Screen.SEND_ADDRESS
private var maxZatoshi: Long? = null
private var availableZatoshi: Long? = null
val sendViewModel: SendViewModel by activityViewModel()
override fun inflate(inflater: LayoutInflater): FragmentSendBinding =
FragmentSendBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Apply View Model
applyViewModel(sendViewModel)
// Apply behaviors
binding.buttonSend.setOnClickListener {
onSubmit().also { tapped(SEND_SUBMIT) }
}
binding.checkIncludeAddress.setOnCheckedChangeListener { _, _->
onIncludeMemo(binding.checkIncludeAddress.isChecked)
}
binding.inputZcashAddress.apply {
doAfterTextChanged {
val textStr = text.toString()
val trim = textStr.trim()
// bugfix: prevent cursor from moving while backspacing and deleting whitespace
if (text.toString() != trim) {
setText(trim)
setSelection(selectionEnd - (textStr.length - trim.length))
}
onAddressChanged(trim)
}
}
binding.backButtonHitArea.onClickNavUp { tapped(SEND_ADDRESS_BACK) }
//
// binding.clearMemo.setOnClickListener {
// onClearMemo().also { tapped(SEND_MEMO_CLEAR) }
// }
binding.inputZcashMemo.doAfterTextChanged {
sendViewModel.memo = binding.inputZcashMemo.text?.toString() ?: ""
onMemoUpdated()
}
binding.textLayoutAddress.setEndIconOnClickListener {
mainActivity?.maybeOpenScan().also { tapped(SEND_ADDRESS_SCAN) }
}
// banners
binding.backgroundClipboard.setOnClickListener {
onPaste().also { tapped(SEND_ADDRESS_PASTE) }
}
binding.containerClipboard.setOnClickListener {
onPaste().also { tapped(SEND_ADDRESS_PASTE) }
}
binding.backgroundLastUsed.setOnClickListener {
onReuse().also { tapped(SEND_ADDRESS_REUSE) }
}
binding.containerLastUsed.setOnClickListener {
onReuse().also { tapped(SEND_ADDRESS_REUSE) }
}
}
private fun applyViewModel(model: SendViewModel) {
// apply amount
val roundedAmount =
model.zatoshiAmount.coerceAtLeast(0L).convertZatoshiToZecStringUniform(8)
binding.textSendAmount.text = "\$$roundedAmount"
// apply address
binding.inputZcashAddress.setText(model.toAddress)
// apply memo
binding.inputZcashMemo.setText(model.memo)
binding.checkIncludeAddress.isChecked = model.includeFromAddress
onMemoUpdated()
}
private fun onMemoUpdated() {
val totalLength = sendViewModel.createMemoToSend().length
binding.textLayoutMemo.helperText = "$totalLength/${ZcashSdk.MAX_MEMO_SIZE} chars"
val color = if (totalLength > ZcashSdk.MAX_MEMO_SIZE) R.color.zcashRed else R.color.text_light_dimmed
binding.textLayoutMemo.setHelperTextColor(ColorStateList.valueOf(color.toAppColor()))
}
private fun onClearMemo() {
binding.inputZcashMemo.setText("")
}
private fun onIncludeMemo(checked: Boolean) {
sendViewModel.afterInitFromAddress {
sendViewModel.includeFromAddress = checked
onMemoUpdated()
tapped(if (checked) SEND_MEMO_INCLUDE else SEND_MEMO_EXCLUDE)
}
}
private fun onAddressChanged(address: String) {
resumedScope.launch {
var type = when (sendViewModel.validateAddress(address)) {
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
}
if (address == sendViewModel.synchronizer.getAddress()) type =
"Warning, this appears to be your address!" to R.color.zcashRed
binding.textLayoutAddress.helperText = 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
if (binding.imageClipboardAddressSelected.isVisible) {
loadAddressFromClipboard().let { clipboardAddress ->
if (address != clipboardAddress) {
updateClipboardBanner(false, clipboardAddress)
}
}
}
// if we have the last used address but we're changing it, then clear the selection
if (binding.imageLastUsedAddressSelected.isVisible) {
loadLastUsedAddress().let { lastAddress ->
if (address != lastAddress) {
updateLastUsedBanner(false, lastAddress)
}
}
}
}
}
private fun onSubmit(unused: EditText? = null) {
sendViewModel.toAddress = binding.inputZcashAddress.text.toString()
sendViewModel.validate(availableZatoshi, maxZatoshi).onFirstWith(resumedScope) { errorMessage ->
if (errorMessage == null) {
mainActivity?.authenticate("Please confirm that you want to send ${sendViewModel.zatoshiAmount.convertZatoshiToZecString(8)} ZEC to\n${sendViewModel.toAddress.toAbbreviatedAddress()}") {
// sendViewModel.funnel(Send.AddressPageComplete)
mainActivity?.safeNavigate(R.id.action_nav_send_to_nav_send_final)
}
} else {
resumedScope.launch {
binding.textAddressError.text = errorMessage
delay(1500L)
binding.textAddressError.text = ""
}
}
}
}
private fun onMax() {
if (maxZatoshi != null) {
// binding.inputZcashAmount.apply {
// setText(maxZatoshi.convertZatoshiToZecString(8))
// postDelayed({
// requestFocus()
// setSelection(text?.length ?: 0)
// }, 10L)
// }
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
mainActivity?.clipboard?.addPrimaryClipChangedListener(this)
}
override fun onDetach() {
super.onDetach()
mainActivity?.clipboard?.removePrimaryClipChangedListener(this)
}
override fun onResume() {
super.onResume()
updateClipboardBanner()
updateLastUsedBanner()
sendViewModel.synchronizer.balances.collectWith(resumedScope) {
onBalanceUpdated(it)
}
binding.inputZcashAddress.text.toString().let {
if (!it.isNullOrEmpty()) onAddressChanged(it)
}
}
private fun onBalanceUpdated(balance: WalletBalance) {
// binding.textLayoutAmount.helperText =
// "You have ${balance.availableZatoshi.coerceAtLeast(0L).convertZatoshiToZecString(8)} available"
maxZatoshi = (balance.availableZatoshi - ZcashSdk.MINERS_FEE_ZATOSHI).coerceAtLeast(0L)
availableZatoshi = balance.availableZatoshi
}
override fun onPrimaryClipChanged() {
twig("clipboard changed!")
updateClipboardBanner()
updateLastUsedBanner()
}
private fun updateClipboardBanner(selected: Boolean = false, address: String? = loadAddressFromClipboard()) {
if (address == null) {
binding.groupClipboard.gone()
} else {
binding.groupClipboard.visible()
binding.clipboardAddress.text = address.toAbbreviatedAddress(16, 16)
binding.imageClipboardAddressSelected.goneIf(!selected)
ImageViewCompat.setImageTintList(binding.imageShield, ColorStateList.valueOf(if (selected) R.color.colorPrimary.toAppColor() else R.color.zcashWhite_12.toAppColor()))
binding.clipboardAddressLabel.setTextColor(if(selected) R.color.colorPrimary.toAppColor() else R.color.text_light.toAppColor())
binding.clipboardAddress.setTextColor(if(selected) R.color.text_light.toAppColor() else R.color.text_light_dimmed.toAppColor())
}
}
private fun updateLastUsedBanner(selected: Boolean = false, address: String? = loadLastUsedAddress()) {
if (address == null || address == loadAddressFromClipboard()) {
binding.groupLastUsed.gone()
} else {
binding.groupLastUsed.visible()
binding.lastUsedAddress.text = address.toAbbreviatedAddress(16, 16)
binding.imageLastUsedAddressSelected.goneIf(!selected)
ImageViewCompat.setImageTintList(binding.imageLastUsedShield, ColorStateList.valueOf(if (selected) R.color.colorPrimary.toAppColor() else R.color.zcashWhite_12.toAppColor()))
binding.lastUsedAddressLabel.setTextColor(if(selected) R.color.colorPrimary.toAppColor() else R.color.text_light.toAppColor())
binding.lastUsedAddress.setTextColor(if(selected) R.color.text_light.toAppColor() else R.color.text_light_dimmed.toAppColor())
}
}
private fun onPaste() {
mainActivity?.clipboard?.let { clipboard ->
if (clipboard.hasPrimaryClip()) {
val address = clipboard.text().toString()
val applyValue = binding.imageClipboardAddressSelected.isGone
updateClipboardBanner(applyValue, address)
binding.inputZcashAddress.setText(address.takeUnless { !applyValue })
}
}
}
private fun onReuse() {
val address = loadLastUsedAddress()
val applyValue = binding.imageLastUsedAddressSelected.isGone
updateLastUsedBanner(applyValue, address)
binding.inputZcashAddress.setText(address.takeUnless { !applyValue })
}
private fun loadAddressFromClipboard(): String? {
mainActivity?.clipboard?.apply {
if (hasPrimaryClip()) {
text()?.let { text ->
if (text.startsWith("zs") && text.length > 70) {
return@loadAddressFromClipboard text.toString()
}
// treat t-addrs differently in the future
if (text.startsWith("t1") && text.length > 32) {
return@loadAddressFromClipboard text.toString()
}
}
}
}
return null
}
private var lastUsedAddress: String? = null
private fun loadLastUsedAddress(): String? {
if (lastUsedAddress == null) sendViewModel.viewModelScope.launch {
lastUsedAddress = sendViewModel.synchronizer.sentTransactions.first().firstOrNull { !it.toAddress.isNullOrEmpty() }?.toAddress
updateLastUsedBanner(binding.imageLastUsedAddressSelected.isVisible, lastUsedAddress)
}
return lastUsedAddress
}
private fun ClipboardManager.text(): CharSequence =
primaryClip!!.getItemAt(0).coerceToText(mainActivity)
}

View File

@ -9,13 +9,12 @@ import cash.z.ecc.android.databinding.FragmentSendMemoBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.gone
import cash.z.ecc.android.ext.goneIf
import cash.z.ecc.android.ext.onClickNavTo
import cash.z.ecc.android.ext.onEditorActionDone
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Funnel.Send
import cash.z.ecc.android.feedback.Report.Tap.*
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIX
import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIX_STANDARD
class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
override val screen = Report.Screen.SEND_MEMO
@ -37,10 +36,10 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
onClearMemo().also { tapped(SEND_MEMO_CLEAR) }
}
R.id.action_nav_send_memo_to_nav_send_address.let {
binding.backButtonHitArea.onClickNavTo(it) { tapped(SEND_MEMO_BACK) }
onBackPressNavTo(it) { tapped(SEND_MEMO_BACK) }
}
// R.id.action_nav_send_memo_to_nav_send_address.let {
// binding.backButtonHitArea.onClickNavTo(it) { tapped(SEND_MEMO_BACK) }
// onBackPressNavTo(it) { tapped(SEND_MEMO_BACK) }
// }
binding.checkIncludeAddress.setOnCheckedChangeListener { _, _->
onIncludeMemo(binding.checkIncludeAddress.isChecked)
@ -56,7 +55,7 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
}
sendViewModel.afterInitFromAddress {
binding.textIncludedAddress.text = "$INCLUDE_MEMO_PREFIX ${sendViewModel.fromAddress}"
binding.textIncludedAddress.text = "$INCLUDE_MEMO_PREFIX_STANDARD ${sendViewModel.fromAddress}"
}
binding.textIncludedAddress.gone()
@ -103,7 +102,7 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
sendViewModel.memo = binding.inputMemo.text.toString()
onNext()
} else {
mainActivity?.safeNavigate(R.id.action_nav_send_memo_to_nav_send_address)
// mainActivity?.safeNavigate(R.id.action_nav_send_memo_to_nav_send_address)
}
}
@ -116,6 +115,6 @@ class SendMemoFragment : BaseFragment<FragmentSendMemoBinding>() {
private fun onNext() {
sendViewModel.funnel(Send.MemoPageComplete)
mainActivity?.safeNavigate(R.id.action_nav_send_memo_to_send_confirm)
// mainActivity?.safeNavigate(R.id.action_nav_send_memo_to_send_confirm)
}
}

View File

@ -13,7 +13,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.ui.setup.WalletSetupViewModel
import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIX
import cash.z.ecc.android.ui.util.INCLUDE_MEMO_PREFIX_STANDARD
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.db.entity.*
@ -23,6 +23,7 @@ import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.validate.AddressType
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ -73,11 +74,17 @@ class SendViewModel @Inject constructor() : ViewModel() {
toAddress,
memoToSend.chunked(ZcashSdk.MAX_MEMO_SIZE).firstOrNull() ?: ""
).onEach {
twig(it.toString())
twig("Received pending txUpdate: ${it?.toString()}")
}
}
fun createMemoToSend() = if (includeFromAddress) "$memo\n$INCLUDE_MEMO_PREFIX\n$fromAddress" else memo
fun cancel(pendingId: Long) {
viewModelScope.launch {
synchronizer.cancelSpend(pendingId)
}
}
fun createMemoToSend() = if (includeFromAddress) "$memo\n$INCLUDE_MEMO_PREFIX_STANDARD\n$fromAddress" else memo
private fun reportIssues(memoToSend: String) {
if (toAddress == fromAddress) feedback.report(Issue.SelfSend)
@ -97,17 +104,26 @@ class SendViewModel @Inject constructor() : ViewModel() {
suspend fun validateAddress(address: String): AddressType =
synchronizer.validateAddress(address)
fun validate(maxZatoshi: Long?) = flow<String?> {
fun validate(availableZatoshi: Long?, maxZatoshi: Long?) = flow<String?> {
when {
synchronizer.validateAddress(toAddress).isNotValid -> {
emit("Please enter a valid address.")
}
zatoshiAmount < 1 -> {
emit("Please enter at least 1 Zatoshi.")
emit("Please go back and enter at least 1 Zatoshi.")
}
availableZatoshi == null -> {
emit("Available funds not found. Please try again in a moment.")
}
availableZatoshi == 0L -> {
emit("No funds available to send.")
}
availableZatoshi > 0 && availableZatoshi < ZcashSdk.MINERS_FEE_ZATOSHI -> {
emit("Insufficient funds to cover miner's fee.")
}
maxZatoshi != null && zatoshiAmount > maxZatoshi -> {
emit( "Please enter no more than ${maxZatoshi.convertZatoshiToZecString(8)} ZEC.")
emit( "Please go back and 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.")

View File

@ -19,12 +19,15 @@ import cash.z.ecc.android.feedback.Report.Tap.BACKUP_DONE
import cash.z.ecc.android.feedback.Report.Tap.BACKUP_VERIFY
import cash.z.ecc.android.feedback.measure
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ -82,10 +85,22 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
override fun onResume() {
super.onResume()
resumedScope.launch {
binding.textBirtdate.text = "Birthday Height: %,d".format(walletSetup.loadBirthdayHeight())
binding.textBirtdate.text = "Birthday Height: %,d".format(calculateBirthday())
}
}
private suspend fun calculateBirthday(): Int {
var storedBirthday: Int = 0
var oldestTransactionHeight:Int = 0
try {
storedBirthday = walletSetup.loadBirthdayHeight()
oldestTransactionHeight = mainActivity?.synchronizerComponent?.synchronizer()?.receivedTransactions?.first()?.last()?.minedHeight ?: 0
} catch (t: Throwable) {
twig("failed to calculate birthday due to: $t")
}
return maxOf(storedBirthday, oldestTransactionHeight, ZcashSdk.SAPLING_ACTIVATION_HEIGHT)
}
private fun onEnterWallet(showMessage: Boolean = !this.hasBackUp) {
if (showMessage) {
Toast.makeText(activity, "Backup verification coming soon!", Toast.LENGTH_LONG).show()

View File

@ -2,8 +2,23 @@ package cash.z.ecc.android.ui.util
import java.nio.charset.StandardCharsets
/**
* The prefix that this wallet uses whenever the user chooses to include their address in the memo.
* This is the one we standardize around.
*/
const val INCLUDE_MEMO_PREFIX_STANDARD = "Reply-To:"
const val INCLUDE_MEMO_PREFIX = "Reply-To:"
/**
* The non-standard prefixes that we will parse if other wallets send them our way.
*/
val INCLUDE_MEMO_PREFIXES_RECOGNIZED = arrayOf(
INCLUDE_MEMO_PREFIX_STANDARD, // standard
"reply-to", // standard w/o colon
"reply to:", // space instead of dash
"reply to", // space instead of dash w/o colon
"sent from:", // previous standard
"sent from" // previous standard w/o colon
)
inline fun ByteArray?.toUtf8Memo(): String {
// TODO: make this more official but for now, this will do

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6 1.41,-1.41z"/>
</vector>

View File

@ -342,6 +342,23 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline_hit_area_top" />
<TextView
android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_height="0dp"
android:text="@string/home_title"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_light"
android:gravity="center"
android:maxLines="1"
app:autoSizeMaxTextSize="16sp"
app:autoSizeMinTextSize="2sp"
app:autoSizeTextType="uniform"
app:layout_constraintTop_toTopOf="@id/icon_logo"
app:layout_constraintBottom_toBottomOf="@id/icon_logo"
app:layout_constraintStart_toEndOf="@id/hit_area_scan"
app:layout_constraintEnd_toStartOf="@id/hit_area_receive" />
<TextView
android:id="@+id/text_detail"
android:layout_width="wrap_content"
@ -349,7 +366,7 @@
android:padding="12dp"
android:elevation="6dp"
android:layout_marginTop="12dp"
android:text="Wallet History"
android:text="@string/home_detail_button_text"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/colorAccent"
android:tint="@color/colorAccent"

View File

@ -0,0 +1,489 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/background_home">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.18" />
<!-- Back Button -->
<ImageView
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android: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" />
<TextView
android:id="@+id/text_send_amount"
android:layout_width="0dp"
android:layout_height="48dp"
android:elevation="6dp"
android:gravity="bottom|center_horizontal"
android:includeFontPadding="false"
android:maxLines="1"
android:paddingBottom="0dp"
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"
app:autoSizeMinTextSize="8sp"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toTopOf="@id/text_banner_message_start"
app:layout_constraintEnd_toStartOf="@id/button_send"
app:layout_constraintStart_toEndOf="@id/spacer_title"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<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:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="@color/text_light"
app:layout_constraintBottom_toTopOf="@id/guideline_content_top"
app:layout_constraintEnd_toStartOf="@id/text_banner_wallet_type"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_send_amount" />
<TextView
android:id="@+id/text_banner_wallet_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="6dp"
android:text="shielded"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="@color/colorPrimary"
app:layout_constraintBaseline_toBaselineOf="@id/text_banner_message_start"
app:layout_constraintEnd_toStartOf="@id/text_banner_message_end"
app:layout_constraintStart_toEndOf="@id/text_banner_message_start" />
<TextView
android:id="@+id/text_banner_message_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="6dp"
android:text=" wallet"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="@color/text_light"
app:layout_constraintBaseline_toBaselineOf="@id/text_banner_message_start"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/text_banner_wallet_type" />
<!-- Down Arrow -->
<ImageView
android:id="@+id/image_down_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="@color/colorPrimary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/text_banner_message_end"
app:layout_constraintStart_toEndOf="@id/text_banner_message_end"
app:layout_constraintTop_toTopOf="@id/text_banner_message_end"
app:srcCompat="@drawable/ic_baseline_keyboard_arrow_down_24" />
<!-- spacer to help with centering the title yet giving it maximum available size -->
<Space
android:id="@+id/spacer_title"
android:layout_width="72dp"
android:layout_height="38dp"
app:layout_constraintBottom_toBottomOf="@id/button_send"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.05"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/button_send" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_send"
android:layout_width="74dp"
android:layout_height="38dp"
android:text="Send"
android:textColor="@color/text_dark"
app:layout_constraintBottom_toBottomOf="@id/back_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.95"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/back_button" />
<TextView
android:id="@+id/text_address_error"
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:paddingEnd="8dp"
android:paddingStart="8dp"
android:textColor="@color/zcashRed"
android:maxLines="1"
android:textSize="14sp"
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_banner_message_start"
tools:text="Please enter a larger amount of money also please enter a shorter sentence" />
<ScrollView
android:id="@+id/scroll_content"
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_address_error">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Input: Address -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_layout_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="To"
android:theme="@style/Zcash.Overlay.TextInputLayout"
app:endIconDrawable="@drawable/ic_qrcode_24dp"
app:endIconMode="custom"
app:helperText="Enter a valid Zcash address"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.08"
app:layout_constraintWidth_percent="0.84">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_zcash_address"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:imeOptions="actionDone"
android:inputType="textNoSuggestions"
android:maxLength="255"
android:singleLine="true"
android:textColor="@color/text_light"
android:textColorHint="@color/text_light_dimmed" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Input: Memo -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_layout_memo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Memo"
android:theme="@style/Zcash.Overlay.TextInputLayout"
app:endIconMode="clear_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_layout_address"
app:layout_constraintWidth_percent="0.84"
tools:helperText="You have 23.23 ZEC available">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/input_zcash_memo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:imeActionLabel="add memo"
android:imeOptions="actionDone"
android:inputType="textMultiLine"
android:maxLength="512"
android:maxLines="3"
android:textColor="@color/text_light"
android:textColorHint="@color/text_light_dimmed"
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" />-->
<!-- Spacer to help position checkbox under the memo line -->
<Space
android:id="@+id/space_checkbox"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="18dp"
app:layout_constraintBottom_toBottomOf="@id/text_layout_memo"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/text_layout_memo" />
<CheckBox
android:id="@+id/check_include_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="0dp"
android:scaleX="0.84"
android:scaleY="0.84"
android:text="include reply-to"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="@color/text_light_dimmed"
android:textSize="14sp"
android:translationY="-10dp"
app:layout_constraintEnd_toEndOf="@id/text_layout_memo"
app:layout_constraintTop_toBottomOf="@id/space_checkbox" />
<!-- -->
<!-- Clipboard items -->
<!-- -->
<View
android:id="@+id/background_clipboard"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/zcashWhite_12"
app:layout_constraintBottom_toBottomOf="@id/divider_clipboard"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/divider_clipboard" />
<TextView
android:id="@+id/divider_clipboard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="1dp"
android:text="On clipboard"
android:textAllCaps="true"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.08"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/check_include_address" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container_clipboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:paddingTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider_clipboard">
<ImageView
android:id="@+id/image_shield"
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_shield"
android:tint="@color/colorPrimary"
app:layout_constraintBottom_toBottomOf="@id/clipboard_address_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.06"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/clipboard_address_label" />
<TextView
android:id="@+id/clipboard_address_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="Unknown"
android:textColor="@color/colorPrimary"
app:layout_constraintBottom_toTopOf="@id/clipboard_address"
app:layout_constraintStart_toEndOf="@id/image_shield"
app:layout_constraintTop_toTopOf="parent"
app:layout_goneMarginStart="0dp" />
<TextView
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" />
<ImageView
android:id="@+id/image_clipboard_address_selected"
android:layout_width="22dp"
android:layout_height="22dp"
android:src="@drawable/ic_baseline_done_24"
android:tint="@color/colorPrimary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias=".95"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- -->
<!-- Last Used items -->
<!-- -->
<View
android:id="@+id/background_last_used"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/zcashWhite_12"
app:layout_constraintBottom_toBottomOf="@id/divider_last_used"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/divider_last_used" />
<TextView
android:id="@+id/divider_last_used"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="1dp"
android:text="last used"
android:textAllCaps="true"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.08"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/container_clipboard" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container_last_used"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider_last_used">
<ImageView
android:id="@+id/image_last_used_shield"
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_shield"
android:tint="@color/colorPrimary"
app:layout_constraintBottom_toBottomOf="@id/last_used_address_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.06"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/last_used_address_label" />
<TextView
android:id="@+id/last_used_address_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="16dp"
android:text="Unknown"
android:textColor="@color/colorPrimary"
app:layout_constraintBottom_toTopOf="@id/last_used_address"
app:layout_constraintStart_toEndOf="@id/image_last_used_shield"
app:layout_constraintTop_toTopOf="parent"
app:layout_goneMarginStart="0dp" />
<TextView
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" />
<ImageView
android:id="@+id/image_last_used_address_selected"
android:layout_width="22dp"
android:layout_height="22dp"
android:src="@drawable/ic_baseline_done_24"
android:tint="@color/colorPrimary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias=".95"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- <TextView-->
<!-- android:id="@+id/text_max"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginBottom="6dp"-->
<!-- android:fontFamily="@font/inconsolata"-->
<!-- android:padding="16dp"-->
<!-- android:text="MAX"-->
<!-- android:textStyle="bold"-->
<!-- app:layout_constraintBottom_toBottomOf="@id/text_layout_memo"-->
<!-- app:layout_constraintEnd_toEndOf="@id/text_layout_memo"-->
<!-- app:layout_constraintTop_toTopOf="@id/text_layout_memo" />-->
<!-- Scan QR code -->
<ImageView
android:id="@+id/image_scan_qr"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="24dp"
android:paddingEnd="1dp"
android:paddingStart="6dp"
android:paddingTop="10dp"
android:tint="@color/zcashWhite"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/text_layout_address"
app:layout_constraintEnd_toEndOf="@id/text_layout_address"
app:layout_constraintTop_toTopOf="@id/text_layout_address"
app:srcCompat="@drawable/ic_qrcode_24dp" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_clipboard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="background_clipboard,divider_clipboard,container_clipboard"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_last_used"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="background_last_used,divider_last_used,container_last_used"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -2,12 +2,11 @@
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:background="@drawable/background_send_final"
android:animateLayoutChanges="true"
>
android:background="@drawable/background_send_final">
<View
android:id="@+id/guide_keys"
@ -28,14 +27,15 @@
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
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"
android:visibility="gone"
app:srcCompat="@drawable/ic_close_black_24dp" />
app:srcCompat="@drawable/ic_close_black_24dp"
tools:visibility="visible" />
<View
android:id="@+id/back_button_hit_area"
@ -53,87 +53,117 @@
android:id="@+id/text_confirmation"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Headline4"
android:gravity="center"
tools:text="Send 12.345 ZEC to\nzs1g7sqw...mvyzgm?"
android:textColor="@color/text_dark"
android:maxLines="3"
style="@style/TextAppearance.MaterialComponents.Headline5"
android:autoSizeTextType="uniform"
android:paddingStart="16dp"
android:gravity="center"
android:maxLines="3"
android:paddingEnd="16dp"
android:paddingStart="16dp"
android:textColor="@color/text_dark"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.21"/>
app:layout_constraintVertical_bias="0.21"
tools:text="Send 12.345 ZEC to\nzs1g7sqw...mvyzgm?" />
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottie_sending"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/button_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.075"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_confirmation"
app:layout_constraintVertical_bias="0.2"
app:layout_constraintWidth_percent="0.3"
app:lottie_autoPlay="true"
app:lottie_loop="true"
app:lottie_rawRes="@raw/lottie_sending" />
<TextView
android:id="@+id/text_status"
android:layout_width="wrap_content"
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"
tools:text="Creating transaction..."
android:gravity="center"
android:textSize="20dp"
app:layout_constraintTop_toBottomOf="@id/radio_include_address"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.1"/>
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintTop_toBottomOf="@id/lottie_sending"
app:layout_constraintBottom_toTopOf="@id/button_primary"
app:layout_constraintStart_toStartOf="@id/button_primary"
app:layout_constraintEnd_toEndOf="@id/button_primary"
/>
<!-- <TextView-->
<!-- android:id="@+id/text_status"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:textColor="@color/text_dark"-->
<!-- tools:text="Creating transaction..."-->
<!-- android:gravity="center"-->
<!-- android:textSize="20dp"-->
<!-- app:layout_constraintTop_toBottomOf="@id/radio_include_address"-->
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
<!-- app:layout_constraintStart_toStartOf="parent"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintVertical_bias="0.1"/>-->
<ProgressBar
android:id="@+id/progress_horizontal"
<!-- <ProgressBar-->
<!-- android:id="@+id/progress_horizontal"-->
<!-- android:layout_width="0dp"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginTop="4dp"-->
<!-- app:layout_constraintStart_toStartOf="@id/text_status"-->
<!-- app:layout_constraintEnd_toEndOf="@id/text_status"-->
<!-- app:layout_constraintTop_toBottomOf="@id/text_status"-->
<!-- android:indeterminate="false"-->
<!-- style="?android:attr/progressBarStyleHorizontal"-->
<!-- android:max="150"-->
<!-- android:foregroundTint="@color/zcashBlack_87" />-->
<!-- <RadioButton-->
<!-- android:id="@+id/radio_include_address"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- style="@style/TextAppearance.MaterialComponents.Body1"-->
<!-- android:text="Includes memo"-->
<!-- android:enabled="false"-->
<!-- android:textColor="@color/text_dark_dimmed"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintStart_toStartOf="parent"-->
<!-- app:layout_constraintTop_toBottomOf="@+id/text_confirmation" />-->
<com.google.android.material.button.MaterialButton
android:id="@+id/button_primary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintStart_toStartOf="@id/text_status"
app:layout_constraintEnd_toEndOf="@id/text_status"
app:layout_constraintTop_toBottomOf="@id/text_status"
android:indeterminate="false"
style="?android:attr/progressBarStyleHorizontal"
android:max="150"
android:foregroundTint="@color/zcashBlack_87" />
style="@style/Zcash.Button.OutlinedButton"
android:padding="20dp"
android:translationY="-6dp"
android:text="@string/cancel"
android:textColor="@color/text_dark"
app:layout_constraintBottom_toTopOf="@id/button_secondary"
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/guide_keys"
app:layout_constraintVertical_bias="0.2"
app:layout_constraintVertical_chainStyle="packed"
app:strokeColor="@color/text_dark" />
<RadioButton
android:id="@+id/radio_include_address"
<TextView
android:id="@+id/button_secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Body1"
android:text="Includes memo"
android:enabled="false"
android:textColor="@color/text_dark_dimmed"
android:layout_marginTop="2dp"
android:padding="12dp"
android:text="@string/done"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/text_dark"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_confirmation" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_retry"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
style="@style/Zcash.Button.OutlinedButton"
app:strokeColor="@color/text_dark"
android:padding="12dp"
android:text="Retry"
android:visibility="gone"
android:textColor="@color/text_dark"
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintBottom_toTopOf="@id/button_next"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_next"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
style="@style/Zcash.Button.OutlinedButton"
app:strokeColor="@color/text_dark"
android:padding="12dp"
android:text="Finished"
android:visibility="gone"
android:textColor="@color/text_dark"
app:layout_constraintEnd_toEndOf="@id/guide_keys"
app:layout_constraintStart_toStartOf="@id/guide_keys"
app:layout_constraintTop_toBottomOf="@id/guide_keys" />
app:layout_constraintTop_toBottomOf="@id/button_primary" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -22,7 +22,7 @@
app:destination="@id/nav_landing" />
<action
android:id="@+id/action_nav_home_to_send"
app:destination="@id/nav_send_address"
app:destination="@id/nav_send"
app:exitAnim="@anim/anim_exit_to_left"
app:enterAnim="@anim/anim_enter_from_right"/>
<action
@ -50,8 +50,8 @@
android:name="cash.z.ecc.android.ui.scan.ScanFragment"
tools:layout="@layout/fragment_scan">
<action
android:id="@+id/action_nav_scan_to_nav_send_address"
app:destination="@id/nav_send_address"
android:id="@+id/action_nav_scan_to_nav_send"
app:destination="@id/nav_send"
app:popUpTo="@id/nav_scan"
app:popUpToInclusive="true"/>
<action
@ -86,57 +86,61 @@
<!-- -->
<fragment
android:id="@+id/nav_send_address"
android:name="cash.z.ecc.android.ui.send.SendAddressFragment"
tools:layout="@layout/fragment_send_address" >
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_address_to_send_memo"
app:destination="@id/nav_send_memo"
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_address_to_nav_scan"
android:id="@+id/action_nav_send_to_nav_scan"
app:destination="@id/nav_scan" />
<action
android:id="@+id/action_nav_send_address_to_nav_home"
android:id="@+id/action_nav_send_to_nav_home"
app:destination="@id/nav_home"
app:enterAnim="@anim/anim_enter_from_left"
app:exitAnim="@anim/anim_exit_to_right"/>
</fragment>
<fragment
android:id="@+id/nav_send_memo"
android:name="cash.z.ecc.android.ui.send.SendMemoFragment"
tools:layout="@layout/fragment_send_memo" >
<action
android:id="@+id/action_nav_send_memo_to_send_confirm"
app:destination="@id/nav_send_confirm"
app:exitAnim="@anim/anim_exit_to_left"
app:enterAnim="@anim/anim_enter_from_right"/>
<action
android:id="@+id/action_nav_send_memo_to_nav_send_address"
app:destination="@id/nav_send_address"
app:enterAnim="@anim/anim_enter_from_left"
app:exitAnim="@anim/anim_exit_to_right" />
</fragment>
<fragment
android:id="@+id/nav_send_confirm"
android:name="cash.z.ecc.android.ui.send.SendConfirmFragment"
tools:layout="@layout/fragment_send_confirm" >
<action
android:id="@+id/action_nav_send_confirm_to_send_final"
android:id="@+id/action_nav_send_to_nav_send_final"
app:destination="@id/nav_send_final"
app:popUpTo="@id/nav_send_confirm"
app:popUpToInclusive="true"
app:enterAnim="@anim/anim_fade_in"/>
<action
android:id="@+id/action_nav_send_confirm_to_nav_send_memo"
app:destination="@id/nav_send_memo"
app:enterAnim="@anim/anim_enter_from_left"
app:exitAnim="@anim/anim_exit_to_right" />
</fragment>
<!-- <fragment-->
<!-- android:id="@+id/nav_send_memo"-->
<!-- android:name="cash.z.ecc.android.ui.send.SendMemoFragment"-->
<!-- tools:layout="@layout/fragment_send_memo" >-->
<!-- <action-->
<!-- android:id="@+id/action_nav_send_memo_to_send_confirm"-->
<!-- app:destination="@id/nav_send_confirm"-->
<!-- app:exitAnim="@anim/anim_exit_to_left"-->
<!-- app:enterAnim="@anim/anim_enter_from_right"/>-->
<!-- <action-->
<!-- android:id="@+id/action_nav_send_memo_to_nav_send_address"-->
<!-- app:destination="@id/nav_send_address"-->
<!-- app:enterAnim="@anim/anim_enter_from_left"-->
<!-- app:exitAnim="@anim/anim_exit_to_right" />-->
<!-- </fragment>-->
<!-- <fragment-->
<!-- android:id="@+id/nav_send_confirm"-->
<!-- android:name="cash.z.ecc.android.ui.send.SendConfirmFragment"-->
<!-- tools:layout="@layout/fragment_send_confirm" >-->
<!-- <action-->
<!-- android:id="@+id/action_nav_send_confirm_to_send_final"-->
<!-- app:destination="@id/nav_send_final"-->
<!-- app:popUpTo="@id/nav_send_confirm"-->
<!-- app:popUpToInclusive="true"-->
<!-- app:enterAnim="@anim/anim_fade_in"/>-->
<!-- <action-->
<!-- android:id="@+id/action_nav_send_confirm_to_nav_send_memo"-->
<!-- app:destination="@id/nav_send_memo"-->
<!-- app:enterAnim="@anim/anim_enter_from_left"-->
<!-- app:exitAnim="@anim/anim_exit_to_right" />-->
<!-- </fragment>-->
<fragment
android:id="@+id/nav_send_final"
android:name="cash.z.ecc.android.ui.send.SendFinalFragment"
@ -145,6 +149,11 @@
android:id="@+id/action_nav_send_final_to_nav_home"
app:destination="@id/nav_home"
app:popUpTo="@id/nav_home"
app:popUpToInclusive="false"/>
<action
android:id="@+id/action_nav_send_final_to_nav_detail"
app:destination="@id/nav_detail"
app:popUpTo="@id/nav_send"
app:popUpToInclusive="true"/>
</fragment>

View File

@ -0,0 +1 @@
{"v":"5.5.8","fr":60,"ip":0,"op":90,"w":170,"h":130,"nm":"loader3","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[0]},{"t":89,"s":[180]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.145,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[34,65,0],"to":[0,-2.917,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.588},"o":{"x":0.732,"y":0},"t":20,"s":[34,47.5,0],"to":[0,0,0],"ti":[-55.765,0.067,0]},{"i":{"x":0.096,"y":1},"o":{"x":0.167,"y":0.414},"t":45,"s":[90.504,121.958,0],"to":[42.185,-0.051,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.984,"y":0},"t":70,"s":[139,40,0],"to":[0,0,0],"ti":[0,-0.833,0]},{"t":90,"s":[139,65,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":20,"s":[20,20]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":45,"s":[15,15]},{"t":70,"s":[20,20]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":3,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.125490196078,0.125490196078,0.133333333333,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('CHANGE COLORS').layer('CHANGE COLOR').content('CHANGE COLOR HERE').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.408,"y":1},"o":{"x":0.518,"y":0},"t":35,"s":[139,65,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.396,"y":1},"o":{"x":0.461,"y":0},"t":59,"s":[94,65,0],"to":[0,0,0],"ti":[0,0,0]},{"t":83,"s":[104,65,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[20,20],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":3,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.125490196078,0.125490196078,0.133333333333,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('CHANGE COLORS').layer('CHANGE COLOR').content('CHANGE COLOR HERE').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.408,"y":1},"o":{"x":0.518,"y":0},"t":32,"s":[104,65,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.396,"y":1},"o":{"x":0.461,"y":0},"t":56,"s":[59,65,0],"to":[0,0,0],"ti":[0,0,0]},{"t":80,"s":[69,65,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[20,20],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":3,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.125490196078,0.125490196078,0.133333333333,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('CHANGE COLORS').layer('CHANGE COLOR').content('CHANGE COLOR HERE').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":90,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[0]},{"t":54,"s":[-90]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.408,"y":1},"o":{"x":0.518,"y":0},"t":30,"s":[69,65,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.396,"y":1},"o":{"x":0.461,"y":0},"t":54,"s":[24,65,0],"to":[0,0,0],"ti":[0,0,0]},{"t":78,"s":[34,65,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[20,20],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":3,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.125490196078,0.125490196078,0.133333333333,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('CHANGE COLORS').layer('CHANGE COLOR').content('CHANGE COLOR HERE').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":0,"op":90,"st":0,"bm":0}],"markers":[]}

View File

@ -1,15 +1,21 @@
<resources>
<string name="app_name">ECC Wallet</string>
<string name="receive_address_title">Your Shielded Address</string>
<!-- Common -->
<string name="done">Done</string>
<string name="cancel">Cancel</string>
<!-- Home -->
<string name="home_detail_button_text">View History</string>
<string name="home_title">Enter an amount to send</string>
<string name="mixpanel_project">a178e1ef062133fc121079cb12fa43c7</string>
<!-- 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>
<!-- Feedback -->
<string name="feedback_question_1">Any details you\'d like to share?</string>
@ -19,4 +25,9 @@
<string name="feedback_hint_2">My balance was . . .</string>
<string name="feedback_hint_3">I\'d like . . .</string>
<!-- Misc -->
<string name="app_name">ECC Wallet</string>
<string name="mixpanel_project">a178e1ef062133fc121079cb12fa43c7</string>
<string name="receive_address_title">Your Shielded Address</string>
</resources>

View File

@ -40,6 +40,10 @@
<item name="strokeColor">@color/zcashWhite</item>
</style>
<style name="Zcash.TextInputLayout" parent="Widget.MaterialComponents.TextInputLayout.FilledBox">
<item name="boxBackgroundColor">@android:color/transparent</item>
</style>
<!-- Text Appearances -->
<style name="Zcash.TextAppearance.NumberPad" parent="TextAppearance.MaterialComponents.Body1">
@ -85,9 +89,10 @@
<item name="cornerFamily">rounded</item>
<item name="cornerSize">0dp</item>
</style>
<!-- Theme Overlays -->
<style name="Zcash.Overlay.TextInputLayout" parent="ThemeOverlay.MaterialComponents">
<item name="shapeAppearanceSmallComponent">@style/Zcash.ShapeAppearance.TextInputLayout</item>
<item name="textInputStyle">@style/Zcash.TextInputLayout</item>
</style>
</resources>

View File

@ -19,6 +19,7 @@ buildscript {
allprojects {
repositories {
mavenLocal()
google()
jcenter()
maven { url 'https://jitpack.io' }

View File

@ -13,6 +13,7 @@ object Deps {
object AndroidX {
const val ANNOTATION = "androidx.annotation:annotation:1.1.0"
const val APPCOMPAT = "androidx.appcompat:appcompat:1.1.0"
const val BIOMETRICS = "androidx.biometric:biometric:1.1.0-alpha01"
const val CONSTRAINT_LAYOUT = "androidx.constraintlayout:constraintlayout:1.1.3"
const val CORE_KTX = "androidx.core:core-ktx:1.1.0"
const val FRAGMENT_KTX = "androidx.fragment:fragment-ktx:1.1.0-beta01"
@ -79,7 +80,7 @@ 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-beta02") {
object Sdk : Version("1.1.0-beta03") {
val MAINNET = "cash.z.ecc.android:sdk-mainnet:$version"
val TESTNET = "cash.z.ecc.android:sdk-testnet:$version"
}