[#1162] Partial transaction history item rework

- Zcash Android SDK v2.0.7 partially adopted. Proper implementaiton will be part of the Send screens rework.
- Partially addresses #1162. More related UI changes on the transaciton history item come in a follow-up PR
- `HistoryItem` composable will be reworked to several more composables as well
- Also note that the history item amount still lacks proper formatting as filed in #1047
- Closes #1236
- Closes #1288
- Closes #1253
This commit is contained in:
Honza Rychnovský 2024-03-13 09:56:49 +01:00 committed by GitHub
parent 9a929c1109
commit d076605444
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 447 additions and 155 deletions

View File

@ -189,7 +189,7 @@ ZCASH_BIP39_VERSION=1.0.7
ZXING_VERSION=3.5.2 ZXING_VERSION=3.5.2
# WARNING: Ensure a non-snapshot version is used before releasing to production. # WARNING: Ensure a non-snapshot version is used before releasing to production.
ZCASH_SDK_VERSION=2.0.6-SNAPSHOT ZCASH_SDK_VERSION=2.0.7
# Toolchain is the Java version used to build the application, which is separate from the # Toolchain is the Java version used to build the application, which is separate from the
# Java version used to run the application. # Java version used to run the application.

View File

@ -6,6 +6,9 @@ import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.ZecSend import cash.z.ecc.android.sdk.model.ZecSend
// TODO [#1285]: Adopt proposal API
// TODO [#1285]: https://github.com/Electric-Coin-Company/zashi-android/issues/1285
@Suppress("deprecation")
suspend fun Synchronizer.send( suspend fun Synchronizer.send(
spendingKey: UnifiedSpendingKey, spendingKey: UnifiedSpendingKey,
send: ZecSend send: ZecSend

View File

@ -298,9 +298,10 @@ fun StyledBalance(
balanceString: String, balanceString: String,
textStyles: Pair<TextStyle, TextStyle>, textStyles: Pair<TextStyle, TextStyle>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
textColor: Color? = null textColor: Color? = null,
prefix: String? = null
) { ) {
val balanceSplit = splitBalance(balanceString) val balanceSplit = splitBalance(balanceString, prefix)
val content = val content =
buildAnnotatedString { buildAnnotatedString {
@ -338,16 +339,20 @@ fun StyledBalance(
} }
} }
private fun splitBalance(balance: String): Pair<String, String> { private fun splitBalance(
Twig.debug { "Balance before split: $balance" } balance: String,
prefix: String?
): Pair<String, String> {
Twig.debug { "Balance before split: $balance, prefix: $prefix" }
@Suppress("MAGIC_CONSTANT", "MagicNumber") @Suppress("MAGIC_CONSTANT", "MagicNumber")
val cutPosition = balance.indexOf(MonetarySeparators.current(Locale.US).decimal) + 4 val cutPosition = balance.indexOf(MonetarySeparators.current(Locale.US).decimal) + 4
val firstPart = val firstPart =
balance.substring( (prefix ?: "") +
startIndex = 0, balance.substring(
endIndex = cutPosition startIndex = 0,
) endIndex = cutPosition
)
val secondPart = val secondPart =
balance.substring( balance.substring(
startIndex = cutPosition startIndex = cutPosition

View File

@ -46,6 +46,7 @@ data class ExtendedColors(
val radioButtonColor: Color, val radioButtonColor: Color,
val radioButtonTextColor: Color, val radioButtonTextColor: Color,
val historyBackgroundColor: Color, val historyBackgroundColor: Color,
val historySendColor: Color,
) { ) {
@Composable @Composable
fun surfaceGradient() = fun surfaceGradient() =

View File

@ -32,7 +32,7 @@ internal object Dark {
val textDescription = Color(0xFF777777) val textDescription = Color(0xFF777777)
val textProgress = Color(0xFF8B8A8A) val textProgress = Color(0xFF8B8A8A)
val aboutTextColor = Color.Unspecified val aboutTextColor = Color(0xFF4E4E4E)
val screenTitleColor = Color(0xFF040404) val screenTitleColor = Color(0xFF040404)
val welcomeAnimationColor = Color(0xFF231F20) val welcomeAnimationColor = Color(0xFF231F20)
val complementaryColor = Color(0xFFF4B728) val complementaryColor = Color(0xFFF4B728)
@ -70,7 +70,7 @@ internal object Dark {
val overlay = Color(0x22000000) val overlay = Color(0x22000000)
val highlight = Color(0xFFFFD800) val highlight = Color(0xFFFFD800)
val dangerous = Color(0xFFEC0008) val dangerous = Color(0xFFFF0B0B)
val onDangerous = Color(0xFFFFFFFF) val onDangerous = Color(0xFFFFFFFF)
val reference = Color(0xFFFFFFFF) val reference = Color(0xFFFFFFFF)
@ -81,6 +81,7 @@ internal object Dark {
val buttonShadowColor = Color(0xFFFFFFFF) val buttonShadowColor = Color(0xFFFFFFFF)
val historyBackgroundColor = Color(0xFFF6F6F6) val historyBackgroundColor = Color(0xFFF6F6F6)
val historySendColor = Color(0xFFF40202)
} }
internal object Light { internal object Light {
@ -139,7 +140,7 @@ internal object Light {
val overlay = Color(0x22000000) val overlay = Color(0x22000000)
val highlight = Color(0xFFFFD800) val highlight = Color(0xFFFFD800)
val dangerous = Color(0xFFEC0008) val dangerous = Color(0xFFFF0B0B)
val onDangerous = Color(0xFFFFFFFF) val onDangerous = Color(0xFFFFFFFF)
val reference = Color(0xFF000000) val reference = Color(0xFF000000)
@ -149,6 +150,7 @@ internal object Light {
val buttonShadowColor = Color(0xFF000000) val buttonShadowColor = Color(0xFF000000)
val historyBackgroundColor = Color(0xFFF6F6F6) val historyBackgroundColor = Color(0xFFF6F6F6)
val historySendColor = Color(0xFFF40202)
} }
internal val DarkColorPalette = internal val DarkColorPalette =
@ -215,6 +217,7 @@ internal val DarkExtendedColorPalette =
radioButtonColor = Dark.radioButtonColor, radioButtonColor = Dark.radioButtonColor,
radioButtonTextColor = Dark.radioButtonTextColor, radioButtonTextColor = Dark.radioButtonTextColor,
historyBackgroundColor = Dark.historyBackgroundColor, historyBackgroundColor = Dark.historyBackgroundColor,
historySendColor = Dark.historySendColor,
) )
internal val LightExtendedColorPalette = internal val LightExtendedColorPalette =
@ -254,9 +257,10 @@ internal val LightExtendedColorPalette =
darkDividerColor = Light.darkDividerColor, darkDividerColor = Light.darkDividerColor,
tabTextColor = Light.tabTextColor, tabTextColor = Light.tabTextColor,
panelBackgroundColor = Light.panelBackgroundColor, panelBackgroundColor = Light.panelBackgroundColor,
radioButtonColor = Dark.radioButtonColor, radioButtonColor = Light.radioButtonColor,
radioButtonTextColor = Dark.radioButtonTextColor, radioButtonTextColor = Light.radioButtonTextColor,
historyBackgroundColor = Dark.historyBackgroundColor, historyBackgroundColor = Light.historyBackgroundColor,
historySendColor = Light.historySendColor,
) )
@Suppress("CompositionLocalAllowlist") @Suppress("CompositionLocalAllowlist")
@ -301,5 +305,6 @@ internal val LocalExtendedColors =
radioButtonColor = Color.Unspecified, radioButtonColor = Color.Unspecified,
radioButtonTextColor = Color.Unspecified, radioButtonTextColor = Color.Unspecified,
historyBackgroundColor = Color.Unspecified, historyBackgroundColor = Color.Unspecified,
historySendColor = Color.Unspecified,
) )
} }

View File

@ -6,10 +6,12 @@ import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.text.PlatformTextStyle import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.googlefonts.Font import androidx.compose.ui.text.googlefonts.Font
import androidx.compose.ui.text.googlefonts.GoogleFont import androidx.compose.ui.text.googlefonts.GoogleFont
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import co.electriccoin.zcash.ui.design.R import co.electriccoin.zcash.ui.design.R
@ -152,6 +154,17 @@ data class BalanceSingleTextStyles(
val second: TextStyle, val second: TextStyle,
) )
@Immutable
data class TransactionItemTextStyles(
val titleRegular: TextStyle,
val titleRunning: TextStyle,
val titleFailed: TextStyle,
val addressCollapsed: TextStyle,
val valueFirstPart: TextStyle,
val valueSecondPart: TextStyle,
val date: TextStyle,
)
@Immutable @Immutable
data class ExtendedTypography( data class ExtendedTypography(
val listItem: TextStyle, val listItem: TextStyle,
@ -172,6 +185,8 @@ data class ExtendedTypography(
val textNavTab: TextStyle, val textNavTab: TextStyle,
val referenceSmall: TextStyle, val referenceSmall: TextStyle,
val radioButton: TextStyle, val radioButton: TextStyle,
// Grouping transaction item text styles to a wrapper class
val transactionItemStyles: TransactionItemTextStyles,
) )
@Suppress("CompositionLocalAllowlist") @Suppress("CompositionLocalAllowlist")
@ -231,10 +246,7 @@ val LocalExtendedTypography =
) )
), ),
addressStyle = addressStyle =
SecondaryTypography.bodyLarge.copy( SecondaryTypography.bodyLarge.copy(),
// TODO [#1032]: Addresses can be shown with "×" symbols
// TODO [#1032]: https://github.com/Electric-Coin-Company/zashi-android/issues/1032
),
aboutText = aboutText =
PrimaryTypography.bodyLarge.copy( PrimaryTypography.bodyLarge.copy(
fontSize = 14.sp, fontSize = 14.sp,
@ -286,6 +298,42 @@ val LocalExtendedTypography =
PrimaryTypography.bodySmall.copy( PrimaryTypography.bodySmall.copy(
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) ),
transactionItemStyles =
TransactionItemTextStyles(
titleRegular =
PrimaryTypography.bodySmall.copy(
fontSize = 13.sp,
fontWeight = FontWeight.Bold
),
titleRunning =
PrimaryTypography.bodySmall.copy(
fontSize = 13.sp,
fontWeight = FontWeight.ExtraBold,
fontStyle = FontStyle.Italic
),
titleFailed =
PrimaryTypography.bodySmall.copy(
fontSize = 13.sp,
fontWeight = FontWeight.ExtraBold,
textDecoration = TextDecoration.LineThrough
),
addressCollapsed =
SecondaryTypography.bodySmall.copy(
fontSize = 13.sp
),
valueFirstPart =
PrimaryTypography.bodySmall.copy(
fontSize = 13.sp
),
valueSecondPart =
PrimaryTypography.bodySmall.copy(
fontSize = 8.sp
),
date =
PrimaryTypography.bodySmall.copy(
fontSize = 13.sp
),
),
) )
} }

View File

@ -7,8 +7,10 @@ import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.PercentDecimal import cash.z.ecc.android.sdk.model.PercentDecimal
import cash.z.ecc.android.sdk.model.Proposal
import cash.z.ecc.android.sdk.model.TransactionOverview import cash.z.ecc.android.sdk.model.TransactionOverview
import cash.z.ecc.android.sdk.model.TransactionRecipient import cash.z.ecc.android.sdk.model.TransactionRecipient
import cash.z.ecc.android.sdk.model.TransactionSubmitResult
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
@ -80,6 +82,13 @@ internal class MockSynchronizer : CloseableSynchronizer {
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.") error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
} }
override suspend fun createProposedTransactions(
proposal: Proposal,
usk: UnifiedSpendingKey
): Flow<TransactionSubmitResult> {
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} yet.")
}
override fun getMemos(transactionOverview: TransactionOverview): Flow<String> { override fun getMemos(transactionOverview: TransactionOverview): Flow<String> {
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.") error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
} }
@ -120,6 +129,24 @@ internal class MockSynchronizer : CloseableSynchronizer {
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.") error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
} }
override suspend fun proposeShielding(
account: Account,
shieldingThreshold: Zatoshi,
memo: String,
transparentReceiver: String?
): Proposal? {
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} yet.")
}
override suspend fun proposeTransfer(
account: Account,
recipient: String,
amount: Zatoshi,
memo: String
): Proposal {
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} yet.")
}
override suspend fun quickRewind() { override suspend fun quickRewind() {
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.") error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
} }
@ -135,6 +162,13 @@ internal class MockSynchronizer : CloseableSynchronizer {
error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.") error("Intentionally not implemented in ${MockSynchronizer::class.simpleName} implementation.")
} }
@Deprecated(
"Upcoming SDK 2.1 will create multiple transactions at once for some recipients.",
replaceWith =
ReplaceWith(
"createProposedTransactions(proposeTransfer(usk.account, toAddress, amount, memo), usk)"
)
)
override suspend fun sendToAddress( override suspend fun sendToAddress(
usk: UnifiedSpendingKey, usk: UnifiedSpendingKey,
amount: Zatoshi, amount: Zatoshi,
@ -144,6 +178,13 @@ internal class MockSynchronizer : CloseableSynchronizer {
return 1 return 1
} }
@Deprecated(
"Upcoming SDK 2.1 will create multiple transactions at once for some recipients.",
replaceWith =
ReplaceWith(
"proposeShielding(usk.account, shieldingThreshold, memo)?.let { createProposedTransactions(it, usk) }"
)
)
override suspend fun shieldFunds( override suspend fun shieldFunds(
usk: UnifiedSpendingKey, usk: UnifiedSpendingKey,
memo: String memo: String

View File

@ -1,22 +1,24 @@
package co.electriccoin.zcash.ui.screen.account.history.fixture package co.electriccoin.zcash.ui.screen.account.history.fixture
import cash.z.ecc.android.sdk.fixture.TransactionOverviewFixture import cash.z.ecc.android.sdk.fixture.TransactionOverviewFixture
import cash.z.ecc.android.sdk.model.TransactionOverview import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.TransactionRecipient
import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState
import co.electriccoin.zcash.ui.screen.account.state.TransactionOverviewExt
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
internal object TransactionHistorySyncStateFixture { internal object TransactionHistorySyncStateFixture {
val TRANSACTIONS = val TRANSACTIONS =
persistentListOf( persistentListOf(
TransactionOverviewFixture.new(), TransactionOverviewExt(TransactionOverviewFixture.new(), TransactionRecipient.Account(Account.DEFAULT)),
TransactionOverviewFixture.new(), TransactionOverviewExt(TransactionOverviewFixture.new(), TransactionRecipient.Account(Account(1))),
TransactionOverviewFixture.new() TransactionOverviewExt(TransactionOverviewFixture.new(), null),
) )
val STATE = TransactionHistorySyncState.Syncing(TRANSACTIONS) val STATE = TransactionHistorySyncState.Syncing(TRANSACTIONS)
fun new( fun new(
transactions: ImmutableList<TransactionOverview> = TRANSACTIONS, transactions: ImmutableList<TransactionOverviewExt> = TRANSACTIONS,
state: TransactionHistorySyncState = STATE state: TransactionHistorySyncState = STATE
) = when (state) { ) = when (state) {
is TransactionHistorySyncState.Syncing -> { is TransactionHistorySyncState.Syncing -> {

View File

@ -110,7 +110,7 @@ class HistoryViewTest {
assertEquals(0, testSetup.getOnItemClickCount()) assertEquals(0, testSetup.getOnItemClickCount())
composeTestRule.onAllNodesWithTag(HistoryTag.TRANSACTION_ITEM, useUnmergedTree = true).also { composeTestRule.onAllNodesWithTag(HistoryTag.TRANSACTION_ITEM_TITLE, useUnmergedTree = true).also {
it.assertCountEquals(TransactionHistorySyncStateFixture.TRANSACTIONS.size) it.assertCountEquals(TransactionHistorySyncStateFixture.TRANSACTIONS.size)
TransactionHistorySyncStateFixture.TRANSACTIONS.forEachIndexed { index, _ -> TransactionHistorySyncStateFixture.TRANSACTIONS.forEachIndexed { index, _ ->

View File

@ -35,6 +35,7 @@ import co.electriccoin.zcash.ui.preference.EncryptedPreferenceSingleton
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState
import co.electriccoin.zcash.ui.screen.account.state.TransactionOverviewExt
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -48,6 +49,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
@ -183,13 +185,22 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
val transactionHistoryState = val transactionHistoryState =
synchronizer synchronizer
.filterNotNull() .filterNotNull()
.flatMapLatest { .flatMapLatest { synchronizer ->
it.transactions synchronizer.transactions
.combine(it.status) { transactions: List<TransactionOverview>, status: Synchronizer.Status -> .combine(synchronizer.status) {
transactions: List<TransactionOverview>, status: Synchronizer.Status ->
val enhancedTransactions =
transactions.map {
if (it.isSentTransaction) {
TransactionOverviewExt(it, synchronizer.getRecipients(it).firstOrNull())
} else {
TransactionOverviewExt(it, null)
}
}
if (status.isSyncing()) { if (status.isSyncing()) {
TransactionHistorySyncState.Syncing(transactions.toPersistentList()) TransactionHistorySyncState.Syncing(enhancedTransactions.toPersistentList())
} else { } else {
TransactionHistorySyncState.Done(transactions.toPersistentList()) TransactionHistorySyncState.Done(enhancedTransactions.toPersistentList())
} }
} }
} }

View File

@ -50,14 +50,14 @@ internal fun WrapAccount(
transactionState = transactionHistoryState, transactionState = transactionHistoryState,
onItemClick = { tx -> onItemClick = { tx ->
Twig.debug { "Transaction item clicked - querying memos..." } Twig.debug { "Transaction item clicked - querying memos..." }
val memos = synchronizer?.getMemos(tx) val memos = synchronizer?.getMemos(tx.overview)
scope.launch { scope.launch {
memos?.toList()?.let { memos?.toList()?.let {
val merged = it.joinToString().ifEmpty { "-" } val merged = it.joinToString().ifEmpty { "-" }
Twig.info { "Transaction memos: count: ${it.size}, contains: $merged" } Twig.info { "Transaction memos: count: ${it.size}, contains: $merged" }
ClipboardManagerUtil.copyToClipboard( ClipboardManagerUtil.copyToClipboard(
activity.applicationContext, activity.applicationContext,
activity.getString(R.string.history_item_clipboard_tag), activity.getString(R.string.account_history_item_clipboard_tag),
merged merged
) )
} }
@ -67,7 +67,7 @@ internal fun WrapAccount(
Twig.debug { "Transaction ID clicked: $txId" } Twig.debug { "Transaction ID clicked: $txId" }
ClipboardManagerUtil.copyToClipboard( ClipboardManagerUtil.copyToClipboard(
activity.applicationContext, activity.applicationContext,
activity.getString(R.string.history_id_clipboard_tag), activity.getString(R.string.account_history_id_clipboard_tag),
txId txId
) )
}, },

View File

@ -5,7 +5,7 @@ package co.electriccoin.zcash.ui.screen.account
*/ */
object HistoryTag { object HistoryTag {
const val TRANSACTION_LIST = "transaction_list" const val TRANSACTION_LIST = "transaction_list"
const val TRANSACTION_ITEM = "transaction_item" const val TRANSACTION_ITEM_TITLE = "transaction_item_title"
const val TRANSACTION_ID = "transaction_id" const val TRANSACTION_ID = "transaction_id"
const val PROGRESS = "progress_bar" const val PROGRESS = "progress_bar"
} }

View File

@ -1,6 +1,5 @@
package co.electriccoin.zcash.ui.screen.account.state package co.electriccoin.zcash.ui.screen.account.state
import cash.z.ecc.android.sdk.model.TransactionOverview
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
sealed class TransactionHistorySyncState { sealed class TransactionHistorySyncState {
@ -8,13 +7,13 @@ sealed class TransactionHistorySyncState {
override fun toString() = "Loading" // NON-NLS override fun toString() = "Loading" // NON-NLS
} }
data class Syncing(val transactions: ImmutableList<TransactionOverview>) : TransactionHistorySyncState() { data class Syncing(val transactions: ImmutableList<TransactionOverviewExt>) : TransactionHistorySyncState() {
fun hasNoTransactions(): Boolean { fun hasNoTransactions(): Boolean {
return transactions.isEmpty() return transactions.isEmpty()
} }
} }
data class Done(val transactions: ImmutableList<TransactionOverview>) : TransactionHistorySyncState() { data class Done(val transactions: ImmutableList<TransactionOverviewExt>) : TransactionHistorySyncState() {
fun hasNoTransactions(): Boolean { fun hasNoTransactions(): Boolean {
return transactions.isEmpty() return transactions.isEmpty()
} }

View File

@ -0,0 +1,9 @@
package co.electriccoin.zcash.ui.screen.account.state
import cash.z.ecc.android.sdk.model.TransactionOverview
import cash.z.ecc.android.sdk.model.TransactionRecipient
data class TransactionOverviewExt(
val overview: TransactionOverview,
val recipient: TransactionRecipient?
)

View File

@ -17,7 +17,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.fixture.TransactionOverviewFixture import cash.z.ecc.android.sdk.fixture.TransactionOverviewFixture
import cash.z.ecc.android.sdk.model.TransactionOverview
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.BalanceWidget import co.electriccoin.zcash.ui.common.compose.BalanceWidget
@ -30,6 +29,7 @@ import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.account.AccountTag import co.electriccoin.zcash.ui.screen.account.AccountTag
import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState
import co.electriccoin.zcash.ui.screen.account.state.TransactionOverviewExt
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@Preview("Account No History") @Preview("Account No History")
@ -64,9 +64,18 @@ private fun HistoryListComposablePreview() {
TransactionHistorySyncState.Syncing( TransactionHistorySyncState.Syncing(
@Suppress("MagicNumber") @Suppress("MagicNumber")
persistentListOf( persistentListOf(
TransactionOverviewFixture.new(netValue = Zatoshi(100000000)), TransactionOverviewExt(
TransactionOverviewFixture.new(netValue = Zatoshi(200000000)), TransactionOverviewFixture.new(netValue = Zatoshi(100000000)),
TransactionOverviewFixture.new(netValue = Zatoshi(300000000)), null
),
TransactionOverviewExt(
TransactionOverviewFixture.new(netValue = Zatoshi(200000000)),
null
),
TransactionOverviewExt(
TransactionOverviewFixture.new(netValue = Zatoshi(300000000)),
null
),
) )
), ),
onItemClick = {}, onItemClick = {},
@ -82,7 +91,7 @@ fun Account(
goBalances: () -> Unit, goBalances: () -> Unit,
goSettings: () -> Unit, goSettings: () -> Unit,
isKeepScreenOnWhileSyncing: Boolean?, isKeepScreenOnWhileSyncing: Boolean?,
onItemClick: (TransactionOverview) -> Unit, onItemClick: (TransactionOverviewExt) -> Unit,
onTransactionIdClick: (String) -> Unit, onTransactionIdClick: (String) -> Unit,
transactionState: TransactionHistorySyncState, transactionState: TransactionHistorySyncState,
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
@ -131,7 +140,7 @@ private fun AccountMainContent(
walletSnapshot: WalletSnapshot, walletSnapshot: WalletSnapshot,
isKeepScreenOnWhileSyncing: Boolean?, isKeepScreenOnWhileSyncing: Boolean?,
goBalances: () -> Unit, goBalances: () -> Unit,
onItemClick: (TransactionOverview) -> Unit, onItemClick: (TransactionOverviewExt) -> Unit,
onTransactionIdClick: (String) -> Unit, onTransactionIdClick: (String) -> Unit,
transactionState: TransactionHistorySyncState, transactionState: TransactionHistorySyncState,
modifier: Modifier = Modifier modifier: Modifier = Modifier

View File

@ -1,5 +1,6 @@
package co.electriccoin.zcash.ui.screen.account.view package co.electriccoin.zcash.ui.screen.account.view
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@ -14,39 +15,43 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.outlined.ArrowCircleDown
import androidx.compose.material.icons.outlined.ArrowCircleUp
import androidx.compose.material.icons.twotone.ArrowCircleDown
import androidx.compose.material.icons.twotone.ArrowCircleUp
import androidx.compose.material3.Divider import androidx.compose.material3.Divider
import androidx.compose.material3.DividerDefaults import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.Center import androidx.compose.ui.Alignment.Companion.Center
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import cash.z.ecc.android.sdk.fixture.TransactionOverviewFixture import cash.z.ecc.android.sdk.fixture.TransactionOverviewFixture
import cash.z.ecc.android.sdk.model.TransactionOverview import cash.z.ecc.android.sdk.model.TransactionOverview
import cash.z.ecc.android.sdk.model.TransactionRecipient
import cash.z.ecc.android.sdk.model.TransactionState import cash.z.ecc.android.sdk.model.TransactionState
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.toZecString import cash.z.ecc.android.sdk.model.toZecString
import cash.z.ecc.sdk.type.ZcashCurrency
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.StyledBalance
import co.electriccoin.zcash.ui.design.component.Tiny import co.electriccoin.zcash.ui.design.component.Tiny
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.account.HistoryTag import co.electriccoin.zcash.ui.screen.account.HistoryTag
import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState
import co.electriccoin.zcash.ui.screen.account.state.TransactionOverviewExt
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import java.text.DateFormat import java.text.DateFormat
@ -77,9 +82,18 @@ private fun ComposableHistoryListPreview() {
TransactionHistorySyncState.Syncing( TransactionHistorySyncState.Syncing(
@Suppress("MagicNumber") @Suppress("MagicNumber")
persistentListOf( persistentListOf(
TransactionOverviewFixture.new(netValue = Zatoshi(100000000)), TransactionOverviewExt(
TransactionOverviewFixture.new(netValue = Zatoshi(200000000)), TransactionOverviewFixture.new(netValue = Zatoshi(100000000)),
TransactionOverviewFixture.new(netValue = Zatoshi(300000000)), null
),
TransactionOverviewExt(
TransactionOverviewFixture.new(netValue = Zatoshi(200000000)),
null
),
TransactionOverviewExt(
TransactionOverviewFixture.new(netValue = Zatoshi(300000000)),
null
),
) )
), ),
onItemClick = {}, onItemClick = {},
@ -93,7 +107,9 @@ val dateFormat: DateFormat by lazy {
SimpleDateFormat.getDateTimeInstance( SimpleDateFormat.getDateTimeInstance(
SimpleDateFormat.MEDIUM, SimpleDateFormat.MEDIUM,
SimpleDateFormat.SHORT, SimpleDateFormat.SHORT,
Locale.getDefault() // TODO [#1171]: Remove default MonetarySeparators locale
// TODO [#1171]: https://github.com/Electric-Coin-Company/zashi-android/issues/1171
Locale.US
) )
} }
@ -101,7 +117,7 @@ val dateFormat: DateFormat by lazy {
@Suppress("LongMethod") @Suppress("LongMethod")
fun HistoryContainer( fun HistoryContainer(
transactionState: TransactionHistorySyncState, transactionState: TransactionHistorySyncState,
onItemClick: (TransactionOverview) -> Unit, onItemClick: (TransactionOverviewExt) -> Unit,
onTransactionIdClick: (String) -> Unit, onTransactionIdClick: (String) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@ -143,141 +159,223 @@ fun HistoryContainer(
@Composable @Composable
private fun HistoryList( private fun HistoryList(
transactions: ImmutableList<TransactionOverview>, transactions: ImmutableList<TransactionOverviewExt>,
onItemClick: (TransactionOverview) -> Unit, onItemClick: (TransactionOverviewExt) -> Unit,
onTransactionIdClick: (String) -> Unit onTransactionIdClick: (String) -> Unit
) { ) {
val currency = ZcashCurrency.getLocalizedName(LocalContext.current) if (transactions.isEmpty()) {
Column {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingXlarge))
LazyColumn( Text(
modifier = Modifier.testTag(HistoryTag.TRANSACTION_LIST) modifier = Modifier.fillMaxWidth(),
) { textAlign = TextAlign.Center,
itemsIndexed(transactions) { _, item -> text = stringResource(id = R.string.account_history_empty),
HistoryItem( style = ZcashTheme.extendedTypography.transactionItemStyles.titleRegular,
transaction = item, color = ZcashTheme.colors.textCommon,
currency = currency, maxLines = 1,
onItemClick = onItemClick, overflow = TextOverflow.Ellipsis,
onIdClick = onTransactionIdClick,
) )
}
} else {
LazyColumn(
modifier = Modifier.testTag(HistoryTag.TRANSACTION_LIST)
) {
itemsIndexed(transactions) { _, item ->
HistoryItem(
transaction = item,
onItemClick = onItemClick,
onIdClick = onTransactionIdClick,
)
Divider( Divider(
color = ZcashTheme.colors.dividerColor, color = ZcashTheme.colors.dividerColor,
thickness = DividerDefaults.Thickness, thickness = DividerDefaults.Thickness,
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.spacingDefault) modifier = Modifier.padding(horizontal = ZcashTheme.dimens.spacingDefault)
) )
}
} }
} }
} }
private enum class ItemExpandedState {
COLLAPSED,
EXPANDED,
EXPANDED_ADDRESS,
EXPANDED_ID
}
const val ADDRESS_IN_TITLE_WIDTH_RATIO = 0.5f
@Composable @Composable
@Suppress("LongMethod") @Suppress("LongMethod", "CyclomaticComplexMethod")
fun HistoryItem( fun HistoryItem(
transaction: TransactionOverview, transaction: TransactionOverviewExt,
currency: String, onItemClick: (TransactionOverviewExt) -> Unit,
onItemClick: (TransactionOverview) -> Unit,
onIdClick: (String) -> Unit, onIdClick: (String) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val transactionTypeText: String val typeText: String
val transactionTypeIcon: ImageVector val textColor: Color
when (transaction.getExtendedState()) { val typeIcon: ImageVector
val textStyle: TextStyle
when (transaction.overview.getExtendedState()) {
TransactionExtendedState.SENT -> { TransactionExtendedState.SENT -> {
transactionTypeText = stringResource(id = R.string.history_item_sent) typeText = stringResource(id = R.string.account_history_item_sent)
transactionTypeIcon = Icons.TwoTone.ArrowCircleUp typeIcon = ImageVector.vectorResource(R.drawable.trx_send_icon)
textColor = MaterialTheme.colorScheme.onBackground
textStyle = ZcashTheme.extendedTypography.transactionItemStyles.titleRegular
} }
TransactionExtendedState.SENDING -> { TransactionExtendedState.SENDING -> {
transactionTypeText = stringResource(id = R.string.history_item_sending) typeText = stringResource(id = R.string.account_history_item_sending)
transactionTypeIcon = Icons.Outlined.ArrowCircleUp typeIcon = ImageVector.vectorResource(R.drawable.trx_send_icon)
textColor = ZcashTheme.colors.textDescription
textStyle = ZcashTheme.extendedTypography.transactionItemStyles.titleRunning
} }
TransactionExtendedState.SEND_FAILED -> {
typeText = stringResource(id = R.string.account_history_item_send_failed)
typeIcon = ImageVector.vectorResource(R.drawable.trx_send_icon)
textColor = ZcashTheme.colors.dangerous
textStyle = ZcashTheme.extendedTypography.transactionItemStyles.titleFailed
}
TransactionExtendedState.RECEIVED -> { TransactionExtendedState.RECEIVED -> {
transactionTypeText = stringResource(id = R.string.history_item_received) typeText = stringResource(id = R.string.account_history_item_received)
transactionTypeIcon = Icons.TwoTone.ArrowCircleDown typeIcon = ImageVector.vectorResource(R.drawable.trx_receive_icon)
textColor = MaterialTheme.colorScheme.onBackground
textStyle = ZcashTheme.extendedTypography.transactionItemStyles.titleRegular
} }
TransactionExtendedState.RECEIVING -> { TransactionExtendedState.RECEIVING -> {
transactionTypeText = stringResource(id = R.string.history_item_receiving) typeText = stringResource(id = R.string.account_history_item_receiving)
transactionTypeIcon = Icons.Outlined.ArrowCircleDown typeIcon = ImageVector.vectorResource(R.drawable.trx_receive_icon)
textColor = ZcashTheme.colors.textDescription
textStyle = ZcashTheme.extendedTypography.transactionItemStyles.titleRunning
} }
TransactionExtendedState.EXPIRED -> { TransactionExtendedState.RECEIVE_FAILED -> {
transactionTypeText = stringResource(id = R.string.history_item_expired) typeText = stringResource(id = R.string.account_history_item_receive_failed)
transactionTypeIcon = Icons.Filled.Cancel typeIcon = ImageVector.vectorResource(R.drawable.trx_receive_icon)
textColor = ZcashTheme.colors.dangerous
textStyle = ZcashTheme.extendedTypography.transactionItemStyles.titleFailed
} }
} }
var expandedState: ItemExpandedState by rememberSaveable {
mutableStateOf(ItemExpandedState.COLLAPSED)
}
Row( Row(
modifier = modifier =
modifier modifier
.then( .then(
Modifier Modifier
.fillMaxWidth()
.clickable { onItemClick(transaction) }
.background(color = ZcashTheme.colors.historyBackgroundColor) .background(color = ZcashTheme.colors.historyBackgroundColor)
.padding(all = ZcashTheme.dimens.spacingDefault) .clickable {
), if (expandedState == ItemExpandedState.COLLAPSED) {
verticalAlignment = Alignment.CenterVertically expandedState = ItemExpandedState.EXPANDED
}
onItemClick(transaction)
}
.padding(all = ZcashTheme.dimens.spacingLarge)
.animateContentSize()
)
) { ) {
Image( Image(
imageVector = transactionTypeIcon, imageVector = typeIcon,
contentDescription = transactionTypeText, contentDescription = typeText,
modifier = Modifier.padding(all = ZcashTheme.dimens.spacingTiny)
) )
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingTiny))
Column(modifier = Modifier.fillMaxWidth()) { Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingDefault))
Column {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Column( Text(
modifier = Modifier.weight(1f) text = typeText,
) { style = textStyle,
Body( color = textColor,
text = transactionTypeText, modifier = Modifier.testTag(HistoryTag.TRANSACTION_ITEM_TITLE)
color = Color.Black, )
modifier = Modifier.testTag(HistoryTag.TRANSACTION_ITEM)
)
val dateString = Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingSmall))
transaction.minedHeight?.let {
transaction.blockTimeEpochSeconds?.let { blockTimeEpochSeconds ->
// * 1000 to covert to millis
@Suppress("MagicNumber")
dateFormat.format(blockTimeEpochSeconds.times(1000L))
} ?: stringResource(id = R.string.history_item_date_not_available)
} ?: stringResource(id = R.string.history_item_date_not_available)
// For now, use the same label for the above missing transaction date
Body( if (transaction.recipient != null && transaction.recipient is TransactionRecipient.Address) {
text = dateString, Text(
text = transaction.recipient.addressValue,
style = ZcashTheme.extendedTypography.transactionItemStyles.addressCollapsed,
color = ZcashTheme.colors.textDescription,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier =
Modifier
.fillMaxWidth(ADDRESS_IN_TITLE_WIDTH_RATIO)
)
} else {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.trx_shielded),
contentDescription = stringResource(id = R.string.account_history_item_shielded)
) )
} }
Column { Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingSmall))
Row(modifier = Modifier.align(alignment = Alignment.End)) {
val zecString =
if (transaction.isSentTransaction) {
"-${transaction.netValue.toZecString()}"
} else {
transaction.netValue.toZecString()
}
Body(text = zecString)
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingTiny)) Spacer(modifier = Modifier.weight(1f))
Body(text = currency) StyledBalance(
} balanceString = transaction.overview.netValue.toZecString(),
} textStyles =
Pair(
first = ZcashTheme.extendedTypography.transactionItemStyles.valueFirstPart,
second = ZcashTheme.extendedTypography.transactionItemStyles.valueSecondPart
),
textColor =
if (transaction.overview.isSentTransaction) {
ZcashTheme.colors.historySendColor
} else {
ZcashTheme.colors.textCommon
},
prefix =
if (transaction.overview.isSentTransaction) {
stringResource(id = R.string.account_history_item_sent_prefix)
} else {
stringResource(id = R.string.account_history_item_received_prefix)
}
)
} }
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
val txId = transaction.txIdString() val dateString =
Tiny( transaction.overview.minedHeight?.let {
text = txId, transaction.overview.blockTimeEpochSeconds?.let { blockTimeEpochSeconds ->
modifier = // * 1000 to covert to millis
Modifier @Suppress("MagicNumber")
.clickable { onIdClick(txId) } dateFormat.format(blockTimeEpochSeconds.times(1000))
.testTag(HistoryTag.TRANSACTION_ID) } ?: "-"
} ?: "-"
// For now, use the same label for the above missing transaction date
Text(
text = dateString,
style = ZcashTheme.extendedTypography.transactionItemStyles.date,
color = ZcashTheme.colors.textDescription,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
if (expandedState >= ItemExpandedState.EXPANDED) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
val txId = transaction.overview.txIdString()
Tiny(
text = txId,
modifier =
Modifier
.clickable { onIdClick(txId) }
.testTag(HistoryTag.TRANSACTION_ID)
)
}
} }
} }
} }
@ -285,15 +383,20 @@ fun HistoryItem(
enum class TransactionExtendedState { enum class TransactionExtendedState {
SENT, SENT,
SENDING, SENDING,
SEND_FAILED,
RECEIVED, RECEIVED,
RECEIVING, RECEIVING,
EXPIRED RECEIVE_FAILED,
} }
private fun TransactionOverview.getExtendedState(): TransactionExtendedState { private fun TransactionOverview.getExtendedState(): TransactionExtendedState {
return when (transactionState) { return when (transactionState) {
TransactionState.Expired -> { TransactionState.Expired -> {
TransactionExtendedState.EXPIRED if (isSentTransaction) {
TransactionExtendedState.SEND_FAILED
} else {
TransactionExtendedState.RECEIVE_FAILED
}
} }
TransactionState.Confirmed -> { TransactionState.Confirmed -> {
if (isSentTransaction) { if (isSentTransaction) {

View File

@ -114,7 +114,12 @@ internal fun WrapBalances(
Twig.debug { "Shielding transparent funds" } Twig.debug { "Shielding transparent funds" }
// Using empty string for memo to clear the default memo prefix value defined in the SDK // Using empty string for memo to clear the default memo prefix value defined in the SDK
runCatching { synchronizer.shieldFunds(spendingKey, "") } runCatching {
// TODO [#1285]: Adopt proposal API
// TODO [#1285]: https://github.com/Electric-Coin-Company/zashi-android/issues/1285
@Suppress("deprecation")
synchronizer.shieldFunds(spendingKey, "")
}
.onSuccess { .onSuccess {
Twig.info { "Shielding transaction id:$it submitted successfully" } Twig.info { "Shielding transaction id:$it submitted successfully" }
setShieldState(ShieldState.None) setShieldState(ShieldState.None)

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="12dp"
android:viewportWidth="18"
android:viewportHeight="12">
<group>
<clip-path
android:pathData="M0,0h17.55v11.1h-17.55z"/>
<path
android:pathData="M17.39,0.09C17.32,0.03 17.22,0 17.13,0H17.04L0.32,4.29C0.13,4.34 0,4.51 0,4.71C0,4.91 0.14,5.07 0.33,5.12L8.59,7.07L16.95,11.06C17.01,11.09 17.07,11.1 17.13,11.1C17.21,11.1 17.29,11.08 17.36,11.03C17.48,10.95 17.56,10.82 17.56,10.67V0.43C17.56,0.3 17.5,0.18 17.4,0.09H17.39ZM16.7,4.73V10L5.68,4.73H16.71H16.7ZM5.33,3.88L16.7,0.97V3.88H5.33ZM2.39,4.73H4.17L5.97,5.58L2.39,4.73Z"
android:fillColor="#000000"/>
</group>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="16dp"
android:viewportWidth="20"
android:viewportHeight="16">
<group>
<clip-path
android:pathData="M0,0h20v16h-20z"/>
<path
android:pathData="M19.92,0.07C19.81,-0.03 19.5,-0 19.31,0.06C13.04,2.2 6.77,4.34 0.5,6.5C0.31,6.56 0.17,6.75 0,6.88C0.13,7.04 0.24,7.25 0.41,7.35C1.57,8.05 2.76,8.72 3.92,9.43C4.15,9.57 4.37,9.85 4.44,10.11C4.93,11.9 5.38,13.7 5.86,15.49C5.91,15.68 6.05,15.96 6.2,15.99C6.35,16.02 6.6,15.85 6.74,15.7C7.68,14.75 8.62,13.79 9.53,12.82C9.79,12.54 9.99,12.48 10.35,12.66C11.64,13.32 12.95,13.93 14.25,14.56C14.39,14.63 14.53,14.69 14.77,14.79C14.89,14.6 15.04,14.43 15.12,14.23C16.74,9.71 18.35,5.18 19.96,0.65C20.02,0.47 20.04,0.16 19.93,0.06L19.92,0.07ZM4.26,8.81C3.28,8.21 2.28,7.64 1.2,7C6.3,5.25 11.28,3.55 16.26,1.84L16.3,1.92C16.16,2.01 16.03,2.1 15.89,2.18C12.27,4.38 8.66,6.57 5.04,8.78C4.76,8.95 4.55,8.99 4.26,8.8V8.81ZM6.71,10.97C6.53,11.87 6.39,12.77 6.12,13.68C6.02,13.3 5.92,12.93 5.82,12.55C5.6,11.69 5.38,10.84 5.15,9.98C5.09,9.76 5.06,9.58 5.32,9.43C8.56,7.48 11.79,5.51 15.03,3.55C15.07,3.53 15.12,3.53 15.3,3.49C14.16,4.44 13.13,5.3 12.09,6.16C11.63,6.54 8.6,8.99 8.14,9.38C8.07,9.44 8,9.5 7.95,9.57C7.62,9.81 7.3,10.08 7.01,10.37C6.85,10.52 6.75,10.77 6.71,10.98V10.97ZM6.81,14.61C7.01,13.42 7.19,12.38 7.38,11.25C8,11.55 8.58,11.83 9.2,12.13C8.41,12.95 7.65,13.73 6.8,14.61H6.81ZM14.48,13.9C12.24,12.81 10.04,11.75 7.77,10.65C8.09,10.38 8.39,10.18 8.62,9.92C11.01,7.93 15.96,3.88 18.35,1.89C18.49,1.77 18.63,1.66 18.86,1.6C17.41,5.68 15.95,9.77 14.48,13.9Z"
android:fillColor="#000000"/>
</group>
</vector>

View File

@ -1,11 +1,17 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="history_item_clipboard_tag">Transaction memo</string>
<string name="history_id_clipboard_tag">Transaction ID</string>
<string name="history_item_sent">Sent</string> <string name="account_history_empty">No transaction history</string>
<string name="history_item_received">Received</string>
<string name="history_item_sending">Sending</string> <string name="account_history_item_sent">Sent</string>
<string name="history_item_receiving">Receiving</string> <string name="account_history_item_received">Received</string>
<string name="history_item_expired">Expired</string> <string name="account_history_item_sending">Sending…</string>
<string name="history_item_date_not_available">Date not available</string> <string name="account_history_item_receiving">Receiving…</string>
<string name="account_history_item_receive_failed">Receive failed</string>
<string name="account_history_item_send_failed">Send failed</string>
<string name="account_history_item_shielded">Shielded transaction</string>
<string name="account_history_item_sent_prefix">-</string>
<string name="account_history_item_received_prefix">+</string>
<string name="account_history_item_clipboard_tag">Transaction memo</string>
<string name="account_history_id_clipboard_tag">Transaction ID</string>
</resources> </resources>

View File

@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="17dp"
android:height="14dp"
android:viewportWidth="17"
android:viewportHeight="14">
<group>
<clip-path
android:pathData="M17,0l-17,0l-0,13.909l17,0z"/>
<path
android:pathData="M4.481,3.052L8.415,1.794C8.472,1.772 8.528,1.772 8.585,1.794L12.519,3.052C12.697,3.108 12.728,3.151 12.728,3.352V7.967C12.728,8.568 12.5,9.182 12.051,9.794C11.707,10.261 11.232,10.73 10.639,11.188C9.643,11.957 8.661,12.429 8.619,12.449C8.54,12.488 8.459,12.488 8.379,12.449C8.338,12.429 7.357,11.957 6.359,11.188C5.766,10.73 5.291,10.26 4.948,9.794C4.498,9.182 4.27,8.568 4.27,7.967V3.352C4.28,3.143 4.337,3.114 4.479,3.052H4.481Z"
android:fillColor="#231F20"/>
<path
android:pathData="M17,13.909V0H13.542V0.96H15.715V12.948H13.542V13.908H17V13.909Z"
android:fillColor="#231F20"/>
<path
android:pathData="M0,0V13.909H3.458V12.95H1.285V0.96H3.458V0H0Z"
android:fillColor="#231F20"/>
</group>
</vector>

View File

@ -10,7 +10,7 @@
<string name="balances_transparent_balance_help_close">I got it!</string> <string name="balances_transparent_balance_help_close">I got it!</string>
<string name="balances_transparent_help_content_description">Show help</string> <string name="balances_transparent_help_content_description">Show help</string>
<string name="balances_transparent_balance_shield">Shield and consolidate funds</string> <string name="balances_transparent_balance_shield">Shield and consolidate funds</string>
<string name="balances_transparent_balance_fee">(Typical fee &lt; <xliff:g id="fee_amount" example="0.001">%1$s <string name="balances_transparent_balance_fee">(Typical Fee &lt; <xliff:g id="fee_amount" example="0.001">%1$s
</xliff:g>)</string> </xliff:g>)</string>
<string name="balances_status_syncing" formatted="true">Syncing…</string> <string name="balances_status_syncing" formatted="true">Syncing…</string>

View File

@ -17,7 +17,7 @@
<xliff:g id="max_bytes" example="500">%2$s</xliff:g> <xliff:g id="max_bytes" example="500">%2$s</xliff:g>
</string> </string>
<string name="send_create">Review</string> <string name="send_create">Review</string>
<string name="send_fee">(Typical fee &lt; <xliff:g id="fee_amount" example="0.001">%1$s</xliff:g>)</string> <string name="send_fee">(Typical Fee &lt; <xliff:g id="fee_amount" example="0.001">%1$s</xliff:g>)</string>
<string name="send_confirmation_amount_and_address_format" formatted="true">Send <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC to <xliff:g id="address" example="zs1g7cqw … mvyzgm">%2$s</xliff:g>?</string> <string name="send_confirmation_amount_and_address_format" formatted="true">Send <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC to <xliff:g id="address" example="zs1g7cqw … mvyzgm">%2$s</xliff:g>?</string>
<string name="send_confirmation_memo_format" formatted="true">Memo: <xliff:g id="memo" example="for Veronika">%1$s</xliff:g></string> <string name="send_confirmation_memo_format" formatted="true">Memo: <xliff:g id="memo" example="for Veronika">%1$s</xliff:g></string>