Merge pull request #191 from zcash/feature/update-send-flow
Feature/update send flow
This commit is contained in:
commit
01860b448d
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 ""}"
|
||||
|
|
|
@ -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)
|
||||
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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 = {}
|
||||
)
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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.")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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":[]}
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
@ -89,5 +93,6 @@
|
|||
<!-- 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>
|
||||
|
|
|
@ -19,6 +19,7 @@ buildscript {
|
|||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
google()
|
||||
jcenter()
|
||||
maven { url 'https://jitpack.io' }
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue