New: Implemented transaction detail view.

First pass complete. What remains is: error state handling, animations and hooking into the send flow.
This commit is contained in:
Kevin Gorham 2020-08-28 04:03:27 -04:00
parent de84bcbe7c
commit 630e7e773a
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
9 changed files with 914 additions and 88 deletions

View File

@ -1,11 +1,10 @@
package cash.z.ecc.android.ext
import android.text.Spannable
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import androidx.core.text.toSpannable
fun CharSequence.toColoredSpan(colorResId: Int, coloredPortion: String): Spannable {
fun CharSequence.toColoredSpan(colorResId: Int, coloredPortion: String): CharSequence {
return toSpannable().apply {
val start = this@toColoredSpan.indexOf(coloredPortion)
setSpan(ForegroundColorSpan(colorResId.toAppColor()), start, start + coloredPortion.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

View File

@ -89,6 +89,7 @@ object Report {
BACKUP,
HOME,
HISTORY("wallet.history"),
TRANSACTION("wallet.transaction"),
LANDING,
PROFILE,
FEEDBACK,
@ -124,6 +125,7 @@ object Report {
HOME_FUND_NOW("home.fund.now"),
HOME_CLEAR_AMOUNT("home.clear.amount"),
HISTORY_BACK("history.back"),
TRANSACTION_BACK("transaction.back"),
PROFILE_CLOSE("profile.close"),
PROFILE_BACKUP("profile.backup"),
PROFILE_VIEW_USER_LOGS("profile.view.user.logs"),

View File

@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.ext.twig
import javax.inject.Inject
import javax.inject.Named
@ -19,6 +20,10 @@ class HistoryViewModel @Inject constructor() : ViewModel() {
val transactions get() = synchronizer.clearedTransactions
val balance get() = synchronizer.balances
val latestHeight get() = synchronizer.latestHeight
var selectedTransaction: ConfirmedTransaction? = null
suspend fun getAddress() = synchronizer.getAddress()

View File

@ -0,0 +1,304 @@
package cash.z.ecc.android.ui.history
import android.content.res.ColorStateList
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.os.Bundle
import android.text.format.DateUtils
import android.text.method.ScrollingMovementMethod
import android.view.LayoutInflater
import android.view.View
import androidx.core.view.ViewCompat
import androidx.lifecycle.lifecycleScope
import androidx.transition.*
import cash.z.ecc.android.R
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.databinding.FragmentTransactionBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.ext.*
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.ext.toAbbreviatedAddress
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.ui.MainActivity
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.util.toUtf8Memo
import kotlinx.coroutines.launch
import java.text.NumberFormat
import java.util.*
class TransactionFragment : BaseFragment<FragmentTransactionBinding>() {
override val screen = Report.Screen.TRANSACTION
private val viewModel: HistoryViewModel by activityViewModel()
var isMemoExpanded: Boolean = false
override fun inflate(inflater: LayoutInflater): FragmentTransactionBinding =
FragmentTransactionBinding.inflate(inflater)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// val transition = TransitionInflater.from(requireContext()).inflateTransition(android.R.transition.move)
// sharedElementEnterTransition = transition
// sharedElementReturnTransition = transition
// sharedElementEnterTransition = createSharedElementTransition()
// sharedElementReturnTransition = createSharedElementTransition()
// sharedElementEnterTransition = ChangeBounds().apply { duration = 1500 }
// sharedElementReturnTransition = ChangeBounds().apply { duration = 1500 }
// enterTransition = Fade().apply {
// duration = 1800
//// slideEdge = Gravity.END
// }
}
private fun createSharedElementTransition(duration: Long = 800L): Transition {
return TransitionSet().apply {
ordering = TransitionSet.ORDERING_TOGETHER
this.duration = duration
// interpolator = PathInterpolatorCompat.create(0.4f, 0f, 0.2f, 1f)
addTransition(ChangeBounds())
addTransition(ChangeClipBounds())
addTransition(ChangeTransform())
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
ViewCompat.setTransitionName(topBoxValue, "test_amount_anim_${viewModel.selectedTransaction!!.id}")
ViewCompat.setTransitionName(topBoxBackground, "test_bg_anim_${viewModel.selectedTransaction!!.id}")
backButtonHitArea.onClickNavBack { tapped(Report.Tap.TRANSACTION_BACK) }
lifecycleScope.launch {
viewModel.selectedTransaction.toUiModel(viewModel.latestHeight).let { uiModel ->
topBoxLabel.text = uiModel.topLabel
topBoxValue.text = uiModel.topValue
bottomBoxLabel.text = uiModel.bottomLabel
bottomBoxValue.text = uiModel.bottomValue
textBlockHeight.text = uiModel.minedHeight
textTimestamp.text = uiModel.timestamp
if (uiModel.iconRotation < 0) {
topBoxIcon.gone()
} else {
topBoxIcon.rotation = uiModel.iconRotation
topBoxIcon.visible()
}
if (!uiModel.isMined) {
textBlockHeight.invisible()
textBlockHeightPrefix.invisible()
}
val exploreOnClick = View.OnClickListener {
uiModel.txId?.let { txId ->
mainActivity?.showFirstUseWarning(
Const.Pref.FIRST_USE_VIEW_TX,
titleResId = R.string.dialog_first_use_view_tx_title,
msgResId = R.string.dialog_first_use_view_tx_message,
positiveResId = R.string.dialog_first_use_view_tx_positive,
negativeResId = R.string.dialog_first_use_view_tx_negative
) {
onLaunchUrl(txId.toTransactionUrl())
}
}
}
buttonExplore.setOnClickListener(exploreOnClick)
textBlockHeight.setOnClickListener(exploreOnClick)
uiModel.fee?.let { subwaySpotFee.visible(); subwayLabelFee.visible(); subwayLabelFee.text = it }
uiModel.source?.let { subwaySpotSource.visible(); subwayLabelSource.visible(); subwayLabelSource.text = it }
uiModel.toAddressLabel()?.let { subwaySpotAddress.visible(); subwayLabelAddress.visible(); subwayLabelAddress.text = it }
uiModel.toAddressClickListener()?.let { subwayLabelAddress.setOnClickListener(it) }
// TODO: remove logic from sections below and add more fields or extension functions to UiModel
uiModel.confirmation?.let {
subwaySpotConfirmations.visible(); subwayLabelConfirmations.visible();
subwayLabelConfirmations.text = it
if (it.equals("confirmed", true)) {
subwayLabelConfirmations.setTextColor(R.color.tx_primary.toAppColor())
} else {
subwayLabelConfirmations.setTextColor(R.color.tx_text_light_dimmed.toAppColor())
}
}
uiModel.memo?.let {
hitAreaMemoSubway.setOnClickListener { _ -> onToggleMemo(!isMemoExpanded, it) }
hitAreaMemoIcon.setOnClickListener { _ -> onToggleMemo(!isMemoExpanded, it) }
subwayLabelMemo.movementMethod = ScrollingMovementMethod()
subwaySpotMemoContent.visible()
subwayLabelMemo.visible()
hitAreaMemoSubway.visible()
onToggleMemo(false)
}
}
}
}
}
val invertingMatrix = ColorMatrixColorFilter(ColorMatrix().apply { setSaturation(0f) })
private fun onToggleMemo(isExpanded: Boolean, memo: String = "") {
twig("onToggleMemo($isExpanded, $memo)")
if (isExpanded) {
twig("setting memo text to: $memo")
binding.subwayLabelMemo.setText(memo)
binding.subwayLabelMemo.invalidate()
// don't impede the ability to scroll
binding.groupMemoIcon.gone()
binding.subwayLabelMemo.backgroundTintList = ColorStateList.valueOf(R.color.tx_text_light_dimmed.toAppColor())
binding.subwaySpotMemoContent.colorFilter = invertingMatrix
binding.subwaySpotMemoContent.rotation = 90.0f
} else {
binding.subwayLabelMemo.setText(getString(R.string.transaction_with_memo))
binding.subwayLabelMemo.invalidate()
twig("setting memo text to: with a memo")
binding.groupMemoIcon.visible()
binding.subwayLabelMemo.backgroundTintList = ColorStateList.valueOf(R.color.tx_primary.toAppColor())
binding.subwaySpotMemoContent.colorFilter = null
binding.subwaySpotMemoContent.rotation = 0.0f
}
isMemoExpanded = isExpanded
}
private fun String.toTransactionUrl(): String {
return "https://explorer.z.cash/tx/$this"
}
private fun UiModel?.toAddressClickListener(): View.OnClickListener? {
return this?.address?.let { addr ->
View.OnClickListener { mainActivity?.copyText(addr, "Address") }
}
}
private fun UiModel?.toAddressLabel(): CharSequence? {
if (this == null || this.address == null || this.isInbound == null) return null
val prefix = getString(
if (isInbound == true) {
R.string.transaction_prefix_from
} else {
R.string.transaction_prefix_to
}
)
return "$prefix ${address?.toAbbreviatedAddress() ?: "Unknown" }".let {
it.toColoredSpan(R.color.tx_text_light_dimmed, if (address == null) it else prefix)
}
}
private suspend fun ConfirmedTransaction?.toUiModel(latestHeight: Int? = null): UiModel = UiModel().apply {
this@toUiModel.let { tx ->
txId = mainActivity?.toTxId(tx?.rawTransactionId)
isInbound = when {
!(tx?.toAddress.isNullOrEmpty()) -> false
tx != null && tx.toAddress.isNullOrEmpty() && tx.value > 0L && tx.minedHeight > 0 -> true
else -> null
}
isMined = tx?.minedHeight != null && tx.minedHeight > ZcashSdk.SAPLING_ACTIVATION_HEIGHT
topValue = if (tx == null) "" else "\$${tx?.value.convertZatoshiToZecString()}"
minedHeight = NumberFormat.getNumberInstance(Locale.getDefault()).format(
tx?.minedHeight ?: 0
)
val flags =
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR or DateUtils.FORMAT_ABBREV_MONTH
timestamp = if (tx == null) "Details Unavailable" else DateUtils.getRelativeDateTimeString(
ZcashWalletApp.instance,
tx.blockTimeInSeconds * 1000,
DateUtils.SECOND_IN_MILLIS,
DateUtils.WEEK_IN_MILLIS,
flags
).toString()
// memo logic
val txMemo = tx?.memo.toUtf8Memo()
if (!txMemo.isNullOrEmpty()) {
memo = txMemo
}
// confirmation logic
// TODO: clean all of this up and remove/improve reliance on `isSufficientlyOld` function. Also, add a constant for the number of confirmations we expect.
tx?.let {
val isMined = it.blockTimeInSeconds != 0L
if (isMined) {
val hasLatestHeight = latestHeight != null && latestHeight > ZcashSdk.SAPLING_ACTIVATION_HEIGHT
if (it.minedHeight > 0 && hasLatestHeight) {
val confirmations = latestHeight!! - it.minedHeight + 1
confirmation = if (confirmations > 10) "Confirmed" else "$confirmations of 10 Confirmations"
} else {
if (!hasLatestHeight && isSufficientlyOld(tx)) {
twig("Warning: could not load latestheight from server to determine confirmations but this transaction is mined and old enough to be considered confirmed")
confirmation = "Confirmed"
} else {
twig("Warning: could not determine confirmation text value so it will be left null!")
confirmation = "Confirmation count temporarily unavailable"
}
}
} else {
confirmation = "Pending"
}
}
val mainActivity = (context as MainActivity)
// inbound v. outbound values
when (isInbound) {
true -> {
topLabel = "You Received"
bottomLabel = "Total Received"
bottomValue = "\$${tx?.value.convertZatoshiToZecString()}"
iconRotation = -45f
source = "to your shielded wallet"
address = mainActivity.extractValidAddress(tx?.memo.toUtf8Memo())
}
false -> {
topLabel = "You Sent"
bottomLabel = "Total Sent"
bottomValue = "\$${tx?.value?.plus(ZcashSdk.MINERS_FEE_ZATOSHI).convertZatoshiToZecString()}"
iconRotation = 135f
fee = "+0.0001 network fee"
source = "from your shielded wallet"
address = tx?.toAddress
}
null -> {
twig("Error: transaction appears to be invalid.")
}
}
}
}
// TODO: determine this in a more generic and technically correct way. For now, this is good enough.
// the goal is just to improve the edge cases where the latest height isn't known but other
// information suggests that the TX is confirmed. We can improve this, later.
private fun isSufficientlyOld(tx: ConfirmedTransaction): Boolean {
val threshold = 75 * 1000 * 25 //approx 25 blocks
val delta = System.currentTimeMillis() / 1000L - tx.blockTimeInSeconds
return tx.minedHeight > ZcashSdk.SAPLING_ACTIVATION_HEIGHT
&& delta < threshold
}
data class UiModel(
var topLabel: String = "",
var topValue: String = "",
var bottomLabel: String = "",
var bottomValue: String = "",
var minedHeight: String = "",
var timestamp: String = "",
var iconRotation: Float = -1f,
var fee: String? = null,
var source: String? = null,
var memo: String? = null,
var address: String? = null,
var isInbound: Boolean? = null,
var isMined: Boolean = false,
var confirmation: String? = null,
var txId: String? = null
)
}

View File

@ -8,17 +8,13 @@ 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.sdk.db.entity.ConfirmedTransaction
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
import java.text.SimpleDateFormat
import java.util.*
@ -29,10 +25,10 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
private val bottomText = itemView.findViewById<TextView>(R.id.text_transaction_bottom)
private val shieldIcon = itemView.findViewById<View>(R.id.image_shield)
private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault())
private val addressRegex = """zs\d\w{65,}""".toRegex()
fun bindTo(transaction: T?) {
(itemView.context as MainActivity).lifecycleScope.launch {
val mainActivity = itemView.context as MainActivity
mainActivity.lifecycleScope.launch {
// update view
var lineOne: String = ""
var lineTwo: String = ""
@ -60,7 +56,7 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
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"
if(!isMined && (expiryHeight != null) && (expiryHeight!! < mainActivity.latestHeight ?: -1)) lineTwo = "Expired"
amountDisplay = "- $amountZec"
if (isMined) {
amountColor = R.color.zcashRed
@ -71,7 +67,7 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
}
}
toAddress.isNullOrEmpty() && value > 0L && minedHeight > 0 -> {
lineOne = getSender(transaction)
lineOne = "${mainActivity.getSender(transaction)} paid you"
lineTwo = "Received $timestamp"
amountDisplay = "+ $amountZec"
amountColor = R.color.zcashGreen
@ -91,7 +87,6 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
}
}
topText.text = lineOne
bottomText.text = lineTwo
amountText.text = amountDisplay
@ -106,71 +101,20 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
}
}
private suspend fun getSender(transaction: ConfirmedTransaction): String {
val memo = transaction.memo.toUtf8Memo()
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?): String? {
if (memo == null || memo.length < 25) return null
// note: cannot use substringAfterLast because we need to ignore case
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 ""}"
MaterialAlertDialogBuilder(itemView.context)
.setMessage(detailsMessage)
.setTitle("Transaction Details")
.setCancelable(true)
.setPositiveButton("Ok") { dialog, _ ->
dialog.dismiss()
}
.setNegativeButton("Copy TX") { dialog, _ ->
(itemView.context as MainActivity).copyText(txId, "Transaction Id")
dialog.dismiss()
}
.show()
}
private fun onTransactionLongPressed(transaction: ConfirmedTransaction) {
(transaction.toAddress ?: extractAddress(transaction.memo.toUtf8Memo()))?.let {
(itemView.context as MainActivity).copyText(it, "Transaction Address")
(itemView.context as MainActivity).apply {
historyViewModel.selectedTransaction = transaction
safeNavigate(R.id.action_nav_history_to_nav_transaction)
}
}
private suspend fun String?.validateAddress(): String? {
if (this == null) return null
return if ((itemView.context as MainActivity).isValidAddress(this)) this else null
private fun onTransactionLongPressed(transaction: ConfirmedTransaction) {
val mainActivity = itemView.context as MainActivity
(transaction.toAddress ?: mainActivity.extractAddress(transaction.memo.toUtf8Memo()))?.let {
mainActivity.copyText(it, "Transaction Address")
}
}
}
private fun ByteArray.toTxId(): String {
val sb = StringBuilder(size * 2)
for(i in (size - 1) downTo 0) {
sb.append(String.format("%02x", this[i]))
}
return sb.toString()
}

View File

@ -0,0 +1,547 @@
<?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">
<!-- -->
<!-- Guidelines -->
<!-- -->
<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.1812" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_keyline_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.054" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_keyline_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.946" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_content_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.8447" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_subway_line"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.16" />
<Space
android:id="@+id/space_spots"
android:layout_width="12dp"
android:layout_height="1dp"
app:layout_constraintEnd_toEndOf="@id/subway_line"
app:layout_constraintStart_toStartOf="@id/subway_line"
app:layout_constraintTop_toTopOf="@id/subway_line" />
<Space
android:id="@+id/space_spots_memo"
android:layout_width="18dp"
android:layout_height="1dp"
app:layout_constraintEnd_toEndOf="@id/subway_line"
app:layout_constraintStart_toStartOf="@id/subway_line"
app:layout_constraintTop_toTopOf="@id/subway_line" />
<!-- -->
<!-- Header -->
<!-- -->
<!-- Close Button -->
<ImageView
android:id="@+id/close_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="@color/zcashWhite_40"
app:layout_constraintBottom_toTopOf="@id/text_timestamp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.05"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_cancel" />
<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_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@string/transaction_title"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
android:textColor="@color/text_light"
app:layout_constraintBottom_toBottomOf="@id/close_button"
app:layout_constraintStart_toEndOf="@id/close_button"
app:layout_constraintTop_toTopOf="@id/close_button" />
<TextView
android:id="@+id/text_timestamp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:autoSizeMaxTextSize="18sp"
android:autoSizeMinTextSize="6dp"
android:autoSizeTextType="uniform"
android:gravity="bottom"
android:maxLines="1"
android:textColor="@color/tx_text_light_dimmed"
android:textSize="18sp"
app:layout_constraintBaseline_toBaselineOf="@id/text_block_height"
app:layout_constraintEnd_toStartOf="@id/text_block_height_prefix"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
tools:text="2020-04-14 5:12am and this is way long" />
<TextView
android:id="@+id/text_block_height_prefix"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/transaction_block_height_prefix"
android:textColor="@color/tx_text_light_dimmed"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="@id/close_button"
app:layout_constraintBottom_toTopOf="@id/padding_bottom"
app:layout_constraintEnd_toStartOf="@id/text_block_height" />
<TextView
android:id="@+id/text_block_height"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:textColor="@color/tx_text_light_dimmed_less"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="@id/close_button"
app:layout_constraintBottom_toTopOf="@id/padding_bottom"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
tools:text="796,798" />
<Space
android:id="@+id/padding_bottom"
android:layout_width="100dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/top_box_border"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintHeight_percent="0.021798" />
<!-- -->
<!-- Content: Top -->
<!-- -->
<!-- %height: 75/734 -->
<View
android:id="@+id/top_box_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#25272B"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintHeight_percent="0.1022"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
app:layout_constraintTop_toTopOf="@id/guideline_content_top" />
<View
android:id="@+id/top_box_border"
android:layout_width="0dp"
android:layout_height="2dp"
android:background="@color/tx_primary"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
app:layout_constraintTop_toTopOf="@id/top_box_background" />
<!-- Icon: BG -->
<!-- %height: 42/734 -->
<View
android:id="@+id/top_box_icon_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/background_circle_solid"
android:backgroundTint="@color/tx_circle_icon_bg"
app:layout_constraintBottom_toBottomOf="@id/top_box_background"
app:layout_constraintDimensionRatio="w,1:1"
app:layout_constraintEnd_toEndOf="@id/top_box_background"
app:layout_constraintHeight_percent="0.0572"
app:layout_constraintHorizontal_bias="0.9556"
app:layout_constraintStart_toStartOf="@id/top_box_background"
app:layout_constraintTop_toTopOf="@id/top_box_background" />
<ImageView
android:id="@+id/top_box_icon"
android:layout_width="0dp"
android:layout_height="0dp"
android:src="@drawable/ic_arrow_back_black_24dp"
android:tint="@color/text_light"
app:layout_constraintBottom_toBottomOf="@id/top_box_icon_background"
app:layout_constraintDimensionRatio="w,1:1"
app:layout_constraintEnd_toEndOf="@id/top_box_icon_background"
app:layout_constraintHeight_percent="0.0408"
app:layout_constraintStart_toStartOf="@id/top_box_icon_background"
app:layout_constraintTop_toTopOf="@id/top_box_icon_background" />
<TextView
android:id="@+id/top_box_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="@color/tx_primary"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/top_box_value"
app:layout_constraintEnd_toEndOf="@id/top_box_background"
app:layout_constraintHorizontal_bias="0.0444"
app:layout_constraintStart_toStartOf="@id/top_box_background"
app:layout_constraintTop_toBottomOf="@id/top_box_border"
app:layout_constraintVertical_chainStyle="packed"
tools:text="You Sent" />
<TextView
android:id="@+id/top_box_value"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginTop="4dp"
android:maxLines="1"
android:textAppearance="@style/Zcash.TextAppearance.Zec"
android:textColor="@color/text_light"
android:textSize="36sp"
app:autoSizeMaxTextSize="36sp"
app:autoSizeMinTextSize="6sp"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="@id/top_box_background"
app:layout_constraintEnd_toStartOf="@id/top_box_icon_background"
app:layout_constraintStart_toStartOf="@id/top_box_label"
app:layout_constraintTop_toBottomOf="@id/top_box_label"
tools:text="$4.32" />
<!-- -->
<!-- Content: Subway -->
<!-- -->
<View
android:id="@+id/subway_line"
android:layout_width="2dp"
android:layout_height="0dp"
android:background="@color/tx_primary"
app:layout_constraintBottom_toTopOf="@id/bottom_box_background"
app:layout_constraintStart_toStartOf="@id/guideline_subway_line"
app:layout_constraintTop_toBottomOf="@id/top_box_background" />
<View
android:id="@+id/subway_spot_fee"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="4dp"
android:background="@drawable/background_circle_solid"
android:backgroundTint="@color/tx_primary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/subway_label_fee"
app:layout_constraintDimensionRatio="h,1:1"
app:layout_constraintEnd_toEndOf="@id/space_spots"
app:layout_constraintStart_toStartOf="@id/space_spots"
app:layout_constraintTop_toTopOf="@id/subway_label_fee" />
<View
android:id="@+id/subway_spot_source"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="4dp"
android:background="@drawable/background_circle_solid"
android:backgroundTint="@color/tx_primary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/subway_label_source"
app:layout_constraintDimensionRatio="h,1:1"
app:layout_constraintEnd_toEndOf="@id/space_spots"
app:layout_constraintStart_toStartOf="@id/space_spots"
app:layout_constraintTop_toTopOf="@id/subway_label_source" />
<ImageView
android:id="@+id/subway_spot_memo_content"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="4dp"
android:src="@drawable/ic_expand_memo_enabled"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/subway_label_memo"
app:layout_constraintDimensionRatio="h,1:1"
app:layout_constraintEnd_toEndOf="@id/space_spots_memo"
app:layout_constraintStart_toStartOf="@id/space_spots_memo"
app:layout_constraintTop_toTopOf="@id/subway_label_memo"
tools:visibility="visible" />
<View
android:id="@+id/subway_spot_address"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="4dp"
android:background="@drawable/background_circle_solid"
android:backgroundTint="@color/tx_primary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/subway_label_address"
app:layout_constraintDimensionRatio="h,1:1"
app:layout_constraintEnd_toEndOf="@id/space_spots"
app:layout_constraintStart_toStartOf="@id/space_spots"
app:layout_constraintTop_toTopOf="@id/subway_label_address" />
<View
android:id="@+id/subway_spot_confirmations"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="4dp"
android:background="@drawable/background_circle_solid"
android:backgroundTint="@color/tx_primary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/subway_label_confirmations"
app:layout_constraintDimensionRatio="h,1:1"
app:layout_constraintEnd_toEndOf="@id/space_spots"
app:layout_constraintStart_toStartOf="@id/space_spots"
app:layout_constraintTop_toTopOf="@id/subway_label_confirmations" />
<TextView
android:id="@+id/subway_label_fee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:textColor="@color/tx_text_light_dimmed"
android:textSize="18dp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/subway_line"
app:layout_constraintTop_toTopOf="@id/subway_line"
tools:text="+0.0001 network fee" />
<TextView
android:id="@+id/subway_label_source"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:textColor="@color/tx_text_light_dimmed"
android:textSize="18dp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/subway_line"
app:layout_constraintTop_toBottomOf="@id/subway_label_fee"
tools:text="from your shielded wallet" />
<TextView
android:id="@+id/spacer_memo_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:paddingEnd="8dp"
android:text="@string/transaction_with_memo"
android:textSize="18dp"
android:visibility="invisible"
app:layout_constraintStart_toStartOf="@id/subway_line"
app:layout_constraintTop_toBottomOf="@id/subway_label_source"
tools:visibility="visible" />
<TextView
android:id="@+id/subway_label_memo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:background="@null"
android:clickable="false"
android:fadeScrollbars="false"
android:maxLines="3"
android:scrollbars="vertical"
android:textColor="@color/tx_text_light_dimmed"
android:textSize="18dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintStart_toStartOf="@id/subway_line"
app:layout_constraintTop_toBottomOf="@id/subway_label_source"
tools:text="this is a memo with 512 characters Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Intege"
tools:visibility="visible" />
<ImageView
android:id="@+id/icon_memo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_memo"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/spacer_memo_icon"
app:layout_constraintStart_toEndOf="@id/spacer_memo_icon"
app:layout_constraintTop_toTopOf="@id/spacer_memo_icon" />
<View
android:id="@+id/hit_area_memo_subway"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/subway_label_memo"
app:layout_constraintEnd_toStartOf="@id/subway_label_memo"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
app:layout_constraintTop_toBottomOf="@id/subway_label_source"
tools:alpha="0.3"
tools:background="@color/zcashRed"
tools:visibility="visible" />
<View
android:id="@+id/hit_area_memo_icon"
android:layout_width="56dp"
android:layout_height="56dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/subway_label_memo"
app:layout_constraintEnd_toEndOf="@id/icon_memo"
app:layout_constraintStart_toEndOf="@id/spacer_memo_icon"
app:layout_constraintTop_toTopOf="@id/hit_area_memo_subway"
tools:alpha="0.3"
tools:background="@color/zcashRed"
tools:visibility="visible" />
<TextView
android:id="@+id/subway_label_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:textColor="@color/text_light"
android:textSize="18dp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@id/subway_line"
app:layout_constraintTop_toBottomOf="@id/subway_label_memo"
tools:text="to zs34jgefi30f...10ijgek234e" />
<TextView
android:id="@+id/subway_label_confirmations"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:layout_marginStart="16dp"
android:textColor="@color/tx_text_light_dimmed"
android:textSize="18dp"
app:layout_constraintBottom_toTopOf="@id/bottom_box_background"
app:layout_constraintStart_toStartOf="@id/subway_line"
tools:text="confirmed" />
<!-- -->
<!-- Content: Bottom -->
<!-- -->
<!-- %height: 75/734 -->
<View
android:id="@+id/bottom_box_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#25272B"
android:transitionName="test_transition"
app:layout_constraintBottom_toBottomOf="@id/guideline_content_bottom"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintHeight_percent="0.1022"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start" />
<View
android:id="@+id/bottom_box_border"
android:layout_width="0dp"
android:layout_height="2dp"
android:background="@color/tx_primary"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
app:layout_constraintTop_toTopOf="@id/bottom_box_background" />
<TextView
android:id="@+id/bottom_box_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="Total Spent"
android:textColor="@color/tx_primary"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/bottom_box_value"
app:layout_constraintEnd_toEndOf="@id/bottom_box_background"
app:layout_constraintHorizontal_bias="0.0444"
app:layout_constraintStart_toStartOf="@id/bottom_box_background"
app:layout_constraintTop_toBottomOf="@id/bottom_box_border"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/bottom_box_value"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="4dp"
android:maxLines="1"
android:text="$4.32"
android:textAppearance="@style/Zcash.TextAppearance.Zec"
android:textColor="@color/text_light"
android:textSize="36sp"
app:autoSizeMaxTextSize="36sp"
app:autoSizeMinTextSize="6sp"
app:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="@id/bottom_box_background"
app:layout_constraintEnd_toEndOf="@id/bottom_box_background"
app:layout_constraintStart_toStartOf="@id/bottom_box_label"
app:layout_constraintTop_toBottomOf="@id/bottom_box_label" />
<!-- -->
<!-- Footer -->
<!-- -->
<com.google.android.material.button.MaterialButton
android:id="@+id/button_explore"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Zcash.Button.OutlinedButton"
android:gravity="center"
android:padding="12dp"
android:text="Explore"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/tx_text_light_dimmed_less"
app:icon="@drawable/ic_baseline_launch_24"
app:iconGravity="textEnd"
app:iconTint="@color/tx_text_light_dimmed_less"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/guideline_keyline_end"
app:layout_constraintStart_toStartOf="@id/guideline_keyline_start"
app:layout_constraintTop_toTopOf="@id/guideline_content_bottom"
app:layout_constraintVertical_bias="0.24"
app:strokeColor="@color/tx_text_light_dimmed_less" />
<!-- -->
<!-- Groups -->
<!-- -->
<androidx.constraintlayout.widget.Group
android:id="@+id/group_memo_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="icon_memo, hit_area_memo_icon, spacer_memo_icon" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -36,13 +36,6 @@
android:id="@+id/nav_receive"
android:name="cash.z.ecc.android.ui.receive.ReceiveFragment"
tools:layout="@layout/fragment_receive_new" >
<action
android:id="@+id/action_nav_receive_to_nav_scan"
app:destination="@id/nav_scan"
app:popUpTo="@id/nav_receive"
app:popUpToInclusive="true"
app:exitAnim="@anim/anim_fade_out_address"
app:enterAnim="@anim/anim_fade_in_scanner"/>
</fragment>
<fragment
android:id="@+id/nav_scan"
@ -53,17 +46,19 @@
app:destination="@id/nav_send"
app:popUpTo="@id/nav_scan"
app:popUpToInclusive="true"/>
<action
android:id="@+id/action_nav_scan_to_nav_receive"
app:popUpTo="@id/nav_scan"
app:popUpToInclusive="true"
app:destination="@id/nav_receive"
app:exitAnim="@anim/anim_fade_out_medium"/>
</fragment>
<fragment
android:id="@+id/nav_detail"
android:name="cash.z.ecc.android.ui.detail.WalletDetailFragment"
tools:layout="@layout/fragment_detail" />
android:id="@+id/nav_history"
android:name="cash.z.ecc.android.ui.history.HistoryFragment"
tools:layout="@layout/fragment_history">
<action
android:id="@+id/action_nav_history_to_nav_transaction"
app:destination="@id/nav_transaction" />
</fragment>
<fragment
android:id="@+id/nav_transaction"
android:name="cash.z.ecc.android.ui.history.TransactionFragment"
tools:layout="@layout/fragment_transaction" />
<fragment
android:id="@+id/nav_profile"
android:name="cash.z.ecc.android.ui.profile.ProfileFragment"

View File

@ -70,4 +70,12 @@
<color name="text_dark_dimmed">@color/zcashBlack_54</color>
<color name="text_shadow">@color/zcashBlack_40</color>
<!-- text : pending design review -->
<!-- these are colors found in designs that fall near but outside the palette but may want to
replace existing palette colors, after design review -->
<color name="tx_text_light_dimmed">#9B9B9B</color>
<color name="tx_text_light_dimmed_less">#D3D3D3</color>
<color name="tx_circle_icon_bg">#494A4C</color>
<color name="tx_primary">#F5A623</color>
</resources>

View File

@ -2,11 +2,19 @@
<!-- Common -->
<string name="done">Done</string>
<string name="cancel">Cancel</string>
<string name="unknown">Unknown</string>
<string name="blank"></string>
<!-- Home -->
<string name="home_history_button_text">View History</string>
<string name="home_title">Enter an amount to send</string>
<!-- Transaction details -->
<string name="transaction_title">Transaction Details</string>
<string name="transaction_block_height_prefix">"from block "</string>
<string name="transaction_with_memo">with a memo</string>
<string name="transaction_prefix_from">reply-to</string>
<string name="transaction_prefix_to">to</string>
<!-- Send Flow -->
<string name="send_hint_input_zcash_address">Enter a shielded Zcash address</string>
@ -16,6 +24,13 @@
<string name="send_pending_button_text">Cancel</string>
<string name="send_failed_button_text">Retry</string>
<string name="send_complete_button_text">See Details</string>
<string name="send_banner_address_user">Your shielded address</string>
<string name="send_banner_address_unknown">@string/unknown</string>
<!-- Address -->
<string name="address_label_shielded">Your Shielded Address</string>
<string name="scan_address_title">Scan Recipient Address</string>
<string name="receive_address_title">Receive Funds</string>
<!-- Feedback -->
<string name="feedback_question_1">Any details you\'d like to share?</string>
@ -25,9 +40,16 @@
<string name="feedback_hint_2">My balance was . . .</string>
<string name="feedback_hint_3">I\'d like . . .</string>
<!-- Dialogs -->
<string name="dialog_not_again">Don\'t show me again</string>
<string name="dialog_first_use_view_tx_title">Potential Privacy Risk</string>
<string name="dialog_first_use_view_tx_positive">View Tx</string>
<string name="dialog_first_use_view_tx_negative">Cancel</string>
<string name="dialog_first_use_view_tx_message">Are you sure you\'d like to leave the app? This could reduce privacy, if you do not trust the destination.</string>
<!-- Misc -->
<string name="app_name">ECC Wallet</string>
<string name="mixpanel_project">a178e1ef062133fc121079cb12fa43c7</string>
<string name="receive_address_title">Your Shielded Address</string>
</resources>