Shielded address rotation

This commit is contained in:
Milan Cerovsky 2025-05-15 11:49:18 +02:00
parent 52a5fd53ff
commit 72534e42f4
9 changed files with 184 additions and 171 deletions

View File

@ -11,6 +11,9 @@ private const val DECIMALS_SHORT = 3
private const val MIN_ZATOSHI_FOR_DOTS_SHORT = Zatoshi.ZATOSHI_PER_ZEC / 1000 private const val MIN_ZATOSHI_FOR_DOTS_SHORT = Zatoshi.ZATOSHI_PER_ZEC / 1000
val Zatoshi.Companion.ZERO: Zatoshi
get() = Zatoshi(0)
fun Zatoshi.toZecStringFull() = fun Zatoshi.toZecStringFull() =
convertZatoshiToZecString( convertZatoshiToZecString(
maxDecimals = DECIMALS_MAX_LONG, maxDecimals = DECIMALS_MAX_LONG,

View File

@ -44,6 +44,8 @@ import co.electriccoin.zcash.ui.common.usecase.MarkTxMemoAsReadUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToAddressBookUseCase import co.electriccoin.zcash.ui.common.usecase.NavigateToAddressBookUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToCoinbaseUseCase import co.electriccoin.zcash.ui.common.usecase.NavigateToCoinbaseUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToErrorUseCase import co.electriccoin.zcash.ui.common.usecase.NavigateToErrorUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToReceiveUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToRequestShieldedUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToTaxExportUseCase import co.electriccoin.zcash.ui.common.usecase.NavigateToTaxExportUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToWalletBackupUseCase import co.electriccoin.zcash.ui.common.usecase.NavigateToWalletBackupUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveAddressBookContactsUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveAddressBookContactsUseCase
@ -192,4 +194,6 @@ val useCaseModule =
singleOf(::NavigateToErrorUseCase) singleOf(::NavigateToErrorUseCase)
factoryOf(::RescanQrUseCase) factoryOf(::RescanQrUseCase)
factoryOf(::ShieldFundsMessageUseCase) factoryOf(::ShieldFundsMessageUseCase)
factoryOf(::NavigateToReceiveUseCase)
factoryOf(::NavigateToRequestShieldedUseCase)
} }

View File

@ -1,14 +1,18 @@
package co.electriccoin.zcash.ui.common.datasource package co.electriccoin.zcash.ui.common.datasource
import android.content.Context import android.content.Context
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.AccountImportSetup import cash.z.ecc.android.sdk.model.AccountImportSetup
import cash.z.ecc.android.sdk.model.AccountPurpose import cash.z.ecc.android.sdk.model.AccountPurpose
import cash.z.ecc.android.sdk.model.AccountUuid
import cash.z.ecc.android.sdk.model.UnifiedAddressRequest
import cash.z.ecc.android.sdk.model.UnifiedFullViewingKey import cash.z.ecc.android.sdk.model.UnifiedFullViewingKey
import cash.z.ecc.android.sdk.model.WalletAddress import cash.z.ecc.android.sdk.model.WalletAddress
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
import cash.z.ecc.android.sdk.model.Zip32AccountIndex import cash.z.ecc.android.sdk.model.Zip32AccountIndex
import cash.z.ecc.sdk.extension.ZERO
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.KeystoneAccount import co.electriccoin.zcash.ui.common.model.KeystoneAccount
import co.electriccoin.zcash.ui.common.model.SaplingInfo import co.electriccoin.zcash.ui.common.model.SaplingInfo
@ -18,12 +22,14 @@ import co.electriccoin.zcash.ui.common.model.WalletAccount
import co.electriccoin.zcash.ui.common.model.ZashiAccount import co.electriccoin.zcash.ui.common.model.ZashiAccount
import co.electriccoin.zcash.ui.common.provider.SelectedAccountUUIDProvider import co.electriccoin.zcash.ui.common.provider.SelectedAccountUUIDProvider
import co.electriccoin.zcash.ui.common.provider.SynchronizerProvider import co.electriccoin.zcash.ui.common.provider.SynchronizerProvider
import co.electriccoin.zcash.ui.design.util.combineToFlow
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -34,8 +40,10 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.retryWhen import kotlinx.coroutines.flow.retryWhen
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -61,6 +69,8 @@ interface AccountDataSource {
seedFingerprint: String, seedFingerprint: String,
index: Long index: Long
): Account ): Account
suspend fun requestNextShieldedAddress()
} }
class AccountDataSourceImpl( class AccountDataSourceImpl(
@ -70,119 +80,54 @@ class AccountDataSourceImpl(
) : AccountDataSource { ) : AccountDataSource {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val requestNextShieldedAddressPipeline = MutableSharedFlow<AccountUuid>()
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private val internalAccounts: Flow<List<InternalAccountWithBalances>?> = override val allAccounts: StateFlow<List<WalletAccount>?> =
synchronizerProvider synchronizerProvider
.synchronizer .synchronizer
.flatMapLatest { synchronizer -> .flatMapLatest { synchronizer ->
synchronizer synchronizer
?.accountsFlow ?.accountsFlow
?.map { accounts -> ?.flatMapLatest { allSdkAccounts ->
accounts?.map { account -> allSdkAccounts
if (account.keySource == KEYSTONE_KEYSOURCE) { ?.map { sdkAccount ->
InternalAccountWithAddresses( combine(
sdkAccount = account, observeUnified(synchronizer, sdkAccount),
unifiedAddress = observeTransparent(synchronizer, sdkAccount),
WalletAddress.Unified.new(synchronizer.getUnifiedAddress(account)), observeSapling(synchronizer, sdkAccount),
saplingAddress = null, observeIsSelected(sdkAccount, allSdkAccounts),
transparentAddress = ) { unified, transparent, sapling, isSelected ->
WalletAddress.Transparent.new(synchronizer.getTransparentAddress(account)), when (sdkAccount.keySource?.lowercase()) {
) KEYSTONE_KEYSOURCE ->
} else { KeystoneAccount(
InternalAccountWithAddresses( sdkAccount = sdkAccount,
sdkAccount = account, unified = unified,
unifiedAddress = transparent = transparent,
WalletAddress.Unified.new(synchronizer.getUnifiedAddress(account)), isSelected = isSelected,
saplingAddress = )
WalletAddress.Sapling.new(synchronizer.getSaplingAddress(account)),
transparentAddress =
WalletAddress.Transparent.new(synchronizer.getTransparentAddress(account)),
)
}
}
}?.flatMapLatest { accountsWithAddresses ->
if (accountsWithAddresses == null) {
flowOf(null)
} else {
synchronizer.walletBalances.map { walletBalances ->
if (walletBalances == null) {
null
} else {
accountsWithAddresses.map { accountWithAddresses ->
val balance =
walletBalances[accountWithAddresses.sdkAccount.accountUuid]
InternalAccountWithBalances( else ->
sdkAccount = accountWithAddresses.sdkAccount, ZashiAccount(
unifiedAddress = accountWithAddresses.unifiedAddress, sdkAccount = sdkAccount,
saplingAddress = accountWithAddresses.saplingAddress, unified = unified,
transparentAddress = accountWithAddresses.transparentAddress, transparent = transparent,
orchardBalance = balance?.orchard ?: createEmptyWalletBalance(), sapling = sapling!!,
transparentBalance = balance?.unshielded ?: Zatoshi.ZERO, isSelected = isSelected,
saplingBalance = balance?.sapling, )
)
} }
} }
} }
} ?.combineToFlow() ?: flowOf(null)
}?.retryWhen { _, attempt -> }
?.retryWhen { _, attempt ->
emit(null) emit(null)
delay(attempt.coerceAtMost(RETRY_DELAY).seconds) delay(attempt.coerceAtMost(RETRY_DELAY).seconds)
true true
} }
?: flowOf(null) ?: flowOf(null)
}.flowOn(Dispatchers.IO) }
.flowOn(Dispatchers.IO)
override val allAccounts: StateFlow<List<WalletAccount>?> =
combine(
internalAccounts,
selectedAccountUUIDProvider.uuid,
) { accounts, uuid ->
accounts
?.map { account ->
when (account.sdkAccount.keySource?.lowercase()) {
KEYSTONE_KEYSOURCE ->
KeystoneAccount(
sdkAccount = account.sdkAccount,
unified =
UnifiedInfo(
address = account.unifiedAddress,
balance = account.orchardBalance
),
transparent =
TransparentInfo(
address = account.transparentAddress,
balance = account.transparentBalance
),
isSelected = account.sdkAccount.accountUuid == uuid || accounts.size == 1,
)
else ->
ZashiAccount(
sdkAccount = account.sdkAccount,
unified =
UnifiedInfo(
address = account.unifiedAddress,
balance = account.orchardBalance
),
transparent =
TransparentInfo(
address = account.transparentAddress,
balance = account.transparentBalance
),
sapling =
SaplingInfo(
address = account.saplingAddress!!,
balance = account.saplingBalance!!
),
isSelected =
uuid == null ||
account.sdkAccount.accountUuid == uuid ||
accounts.size == 1,
)
}
}?.sortedDescending()
}.flowOn(Dispatchers.IO)
.stateIn( .stateIn(
scope = scope, scope = scope,
started = SharingStarted.Eagerly, started = SharingStarted.Eagerly,
@ -201,20 +146,11 @@ class AccountDataSourceImpl(
account?.filterIsInstance<ZashiAccount>()?.firstOrNull() account?.filterIsInstance<ZashiAccount>()?.firstOrNull()
}.distinctUntilChanged() }.distinctUntilChanged()
override suspend fun getAllAccounts() = override suspend fun getAllAccounts() = withContext(Dispatchers.IO) { allAccounts.filterNotNull().first() }
withContext(Dispatchers.IO) {
allAccounts.filterNotNull().first()
}
override suspend fun getSelectedAccount() = override suspend fun getSelectedAccount() = withContext(Dispatchers.IO) { selectedAccount.filterNotNull().first() }
withContext(Dispatchers.IO) {
selectedAccount.filterNotNull().first()
}
override suspend fun getZashiAccount() = override suspend fun getZashiAccount() = withContext(Dispatchers.IO) { zashiAccount.filterNotNull().first() }
withContext(Dispatchers.IO) {
zashiAccount.filterNotNull().first()
}
override suspend fun selectAccount(account: Account) { override suspend fun selectAccount(account: Account) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -246,34 +182,76 @@ class AccountDataSourceImpl(
), ),
) )
} }
override suspend fun requestNextShieldedAddress() {
scope
.launch {
requestNextShieldedAddressPipeline.emit(getSelectedAccount().sdkAccount.accountUuid)
}
.join()
}
private fun observeIsSelected(sdkAccount: Account, allAccounts: List<Account>) = selectedAccountUUIDProvider
.uuid
.map { uuid ->
when (sdkAccount.keySource?.lowercase()) {
KEYSTONE_KEYSOURCE -> sdkAccount.accountUuid == uuid || allAccounts.size == 1
else -> uuid == null || sdkAccount.accountUuid == uuid || allAccounts.size == 1
}
}
private suspend fun observeUnified(synchronizer: Synchronizer, sdkAccount: Account): Flow<UnifiedInfo> {
return combine(
requestNextShieldedAddressPipeline
.onStart { emit(sdkAccount.accountUuid) }
.map {
WalletAddress.Unified.new(
synchronizer.getCustomUnifiedAddress(sdkAccount, UnifiedAddressRequest.SHIELDED)
)
},
synchronizer.walletBalances
) { address, balances ->
val balance = balances?.get(sdkAccount.accountUuid)
UnifiedInfo(
address = address,
balance = balance?.orchard ?: createEmptyWalletBalance()
)
}
}
private suspend fun observeTransparent(synchronizer: Synchronizer, sdkAccount: Account): Flow<TransparentInfo> {
val address = WalletAddress.Transparent.new(synchronizer.getTransparentAddress(sdkAccount))
return synchronizer.walletBalances.map {
val balance = it?.get(sdkAccount.accountUuid)
TransparentInfo(
address = address,
balance = balance?.unshielded ?: Zatoshi.ZERO
)
}
}
private suspend fun observeSapling(synchronizer: Synchronizer, sdkAccount: Account): Flow<SaplingInfo?> {
return if (sdkAccount.keySource == KEYSTONE_KEYSOURCE) {
flowOf(null)
} else {
val address = WalletAddress.Sapling.new(synchronizer.getSaplingAddress(sdkAccount))
synchronizer.walletBalances.map {
val balance = it?.get(sdkAccount.accountUuid)
SaplingInfo(
address = address,
balance = balance?.sapling ?: createEmptyWalletBalance()
)
}
}
}
private fun createEmptyWalletBalance() =
WalletBalance(
available = Zatoshi.ZERO,
changePending = Zatoshi.ZERO,
valuePending = Zatoshi.ZERO,
)
} }
private fun createEmptyWalletBalance() =
WalletBalance(
available = Zatoshi.ZERO,
changePending = Zatoshi.ZERO,
valuePending = Zatoshi.ZERO,
)
private val Zatoshi.Companion.ZERO: Zatoshi
get() = Zatoshi(0)
private data class InternalAccountWithAddresses(
val sdkAccount: Account,
val unifiedAddress: WalletAddress.Unified,
val saplingAddress: WalletAddress.Sapling?,
val transparentAddress: WalletAddress.Transparent,
)
private data class InternalAccountWithBalances(
val sdkAccount: Account,
val unifiedAddress: WalletAddress.Unified,
val saplingAddress: WalletAddress.Sapling?,
val transparentAddress: WalletAddress.Transparent,
val saplingBalance: WalletBalance?,
val orchardBalance: WalletBalance,
val transparentBalance: Zatoshi,
)
private const val RETRY_DELAY = 3L private const val RETRY_DELAY = 3L
private const val KEYSTONE_KEYSOURCE = "keystone" private const val KEYSTONE_KEYSOURCE = "keystone"

View File

@ -0,0 +1,22 @@
package co.electriccoin.zcash.ui.common.usecase
import co.electriccoin.zcash.ui.NavigationRouter
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
import co.electriccoin.zcash.ui.common.repository.BiometricRepository
import co.electriccoin.zcash.ui.common.repository.BiometricRequest
import co.electriccoin.zcash.ui.common.repository.BiometricsCancelledException
import co.electriccoin.zcash.ui.common.repository.BiometricsFailureException
import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.screen.addressbook.AddressBookArgs
import co.electriccoin.zcash.ui.screen.receive.Receive
class NavigateToReceiveUseCase(
private val navigationRouter: NavigationRouter,
private val accountDataSource: AccountDataSource
) {
suspend operator fun invoke() {
accountDataSource.requestNextShieldedAddress()
navigationRouter.forward(Receive)
}
}

View File

@ -0,0 +1,18 @@
package co.electriccoin.zcash.ui.common.usecase
import co.electriccoin.zcash.ui.NavigationRouter
import co.electriccoin.zcash.ui.NavigationTargets
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType
class NavigateToRequestShieldedUseCase(
private val navigationRouter: NavigationRouter,
private val accountDataSource: AccountDataSource
) {
suspend operator fun invoke(requestNewAddress: Boolean = true) {
if (requestNewAddress) {
accountDataSource.requestNextShieldedAddress()
}
navigationRouter.forward("${NavigationTargets.REQUEST}/${ReceiveAddressType.Unified.ordinal}")
}
}

View File

@ -18,6 +18,8 @@ import co.electriccoin.zcash.ui.common.usecase.GetSelectedWalletAccountUseCase
import co.electriccoin.zcash.ui.common.usecase.IsRestoreSuccessDialogVisibleUseCase import co.electriccoin.zcash.ui.common.usecase.IsRestoreSuccessDialogVisibleUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToCoinbaseUseCase import co.electriccoin.zcash.ui.common.usecase.NavigateToCoinbaseUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToErrorUseCase import co.electriccoin.zcash.ui.common.usecase.NavigateToErrorUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToReceiveUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToRequestShieldedUseCase
import co.electriccoin.zcash.ui.common.usecase.ShieldFundsMessageUseCase import co.electriccoin.zcash.ui.common.usecase.ShieldFundsMessageUseCase
import co.electriccoin.zcash.ui.design.component.BigIconButtonState import co.electriccoin.zcash.ui.design.component.BigIconButtonState
import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.design.util.stringRes
@ -39,7 +41,6 @@ import co.electriccoin.zcash.ui.screen.home.syncing.WalletSyncingMessageState
import co.electriccoin.zcash.ui.screen.home.updating.WalletUpdatingInfo import co.electriccoin.zcash.ui.screen.home.updating.WalletUpdatingInfo
import co.electriccoin.zcash.ui.screen.home.updating.WalletUpdatingMessageState import co.electriccoin.zcash.ui.screen.home.updating.WalletUpdatingMessageState
import co.electriccoin.zcash.ui.screen.integrations.DialogIntegrations import co.electriccoin.zcash.ui.screen.integrations.DialogIntegrations
import co.electriccoin.zcash.ui.screen.receive.Receive
import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType
import co.electriccoin.zcash.ui.screen.scan.Scan import co.electriccoin.zcash.ui.screen.scan.Scan
import co.electriccoin.zcash.ui.screen.scan.ScanFlow import co.electriccoin.zcash.ui.screen.scan.ScanFlow
@ -61,9 +62,11 @@ class HomeViewModel(
shieldFundsInfoProvider: ShieldFundsInfoProvider, shieldFundsInfoProvider: ShieldFundsInfoProvider,
private val navigationRouter: NavigationRouter, private val navigationRouter: NavigationRouter,
private val isRestoreSuccessDialogVisible: IsRestoreSuccessDialogVisibleUseCase, private val isRestoreSuccessDialogVisible: IsRestoreSuccessDialogVisibleUseCase,
private val navigateToCoinbase: NavigateToCoinbaseUseCase,
private val shieldFunds: ShieldFundsMessageUseCase, private val shieldFunds: ShieldFundsMessageUseCase,
private val navigateToCoinbase: NavigateToCoinbaseUseCase,
private val navigateToError: NavigateToErrorUseCase, private val navigateToError: NavigateToErrorUseCase,
private val navigateToReceive: NavigateToReceiveUseCase,
private val navigateToRequestShielded: NavigateToRequestShieldedUseCase
) : ViewModel() { ) : ViewModel() {
private val messageState = private val messageState =
combine( combine(
@ -234,14 +237,13 @@ class HomeViewModel(
private fun onSendButtonClick() = navigationRouter.forward(Send()) private fun onSendButtonClick() = navigationRouter.forward(Send())
private fun onReceiveButtonClick() = navigationRouter.forward(Receive) private fun onReceiveButtonClick() = viewModelScope.launch { navigateToReceive() }
private fun onScanButtonClick() = navigationRouter.forward(Scan(ScanFlow.HOMEPAGE)) private fun onScanButtonClick() = navigationRouter.forward(Scan(ScanFlow.HOMEPAGE))
private fun onBuyClick() = viewModelScope.launch { navigateToCoinbase(replaceCurrentScreen = false) } private fun onBuyClick() = viewModelScope.launch { navigateToCoinbase(replaceCurrentScreen = false) }
private fun onRequestClick() = private fun onRequestClick() = viewModelScope.launch { navigateToRequestShielded() }
navigationRouter.forward("${NavigationTargets.REQUEST}/${ReceiveAddressType.Unified.ordinal}")
private fun onWalletUpdatingMessageClick() = navigationRouter.forward(WalletUpdatingInfo) private fun onWalletUpdatingMessageClick() = navigationRouter.forward(WalletUpdatingInfo)

View File

@ -46,6 +46,7 @@ import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.util.getValue import co.electriccoin.zcash.ui.design.util.getValue
import co.electriccoin.zcash.ui.design.util.orDark
import co.electriccoin.zcash.ui.design.util.scaffoldScrollPadding import co.electriccoin.zcash.ui.design.util.scaffoldScrollPadding
import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.fixture.ZashiMainTopAppBarStateFixture import co.electriccoin.zcash.ui.fixture.ZashiMainTopAppBarStateFixture
@ -140,9 +141,9 @@ private fun AddressPanel(
.wrapContentHeight() .wrapContentHeight()
.background( .background(
if (state.isShielded) { if (state.isShielded) {
ZashiColors.Utility.Purple.utilityPurple50 ZashiColors.Utility.Purple.utilityPurple50 orDark ZashiColors.Utility.Indigo.utilityIndigo50
} else { } else {
ZashiColors.Utility.Gray.utilityGray50 ZashiColors.Surfaces.bgSecondary
}, },
RoundedCornerShape(ZashiDimensions.Radius.radius3xl) RoundedCornerShape(ZashiDimensions.Radius.radius3xl)
).clip(RoundedCornerShape(ZashiDimensions.Radius.radius3xl)) ).clip(RoundedCornerShape(ZashiDimensions.Radius.radius3xl))
@ -152,7 +153,7 @@ private fun AddressPanel(
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
Box { Box {
Image( Image(
modifier = Modifier.sizeIn(maxWidth = 34.dp, maxHeight = 34.dp), modifier = Modifier.size(40.dp),
painter = painterResource(id = state.icon), painter = painterResource(id = state.icon),
contentDescription = null contentDescription = null
) )
@ -162,7 +163,7 @@ private fun AddressPanel(
Modifier Modifier
.size(14.dp) .size(14.dp)
.align(Alignment.BottomEnd) .align(Alignment.BottomEnd)
.offset(3.5.dp, 3.5.dp), .offset(1.5.dp, .5.dp),
painter = painterResource(R.drawable.ic_receive_shield), painter = painterResource(R.drawable.ic_receive_shield),
contentDescription = "", contentDescription = "",
) )
@ -178,9 +179,7 @@ private fun AddressPanel(
style = ZashiTypography.textMd, style = ZashiTypography.textMd,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
Spacer(4.dp)
Spacer(Modifier.height(ZcashTheme.dimens.spacingTiny))
Text( Text(
text = state.subtitle.getValue(), text = state.subtitle.getValue(),
color = ZashiColors.Text.textTertiary, color = ZashiColors.Text.textTertiary,
@ -191,13 +190,6 @@ private fun AddressPanel(
Spacer(Modifier.width(ZcashTheme.dimens.spacingSmall)) Spacer(Modifier.width(ZcashTheme.dimens.spacingSmall))
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
if (state.isShielded) {
Image(
painter = painterResource(id = R.drawable.ic_check_shielded_solid),
contentDescription = null
)
}
} }
AnimatedVisibility(visible = state.isExpanded) { AnimatedVisibility(visible = state.isExpanded) {
@ -210,13 +202,13 @@ private fun AddressPanel(
) { ) {
val containerColor = val containerColor =
if (state.isShielded) { if (state.isShielded) {
ZashiColors.Utility.Purple.utilityPurple100 ZashiColors.Utility.Purple.utilityPurple100 orDark ZashiColors.Utility.Indigo.utilityIndigo100
} else { } else {
ZashiColors.Surfaces.bgTertiary ZashiColors.Surfaces.bgTertiary
} }
val contentColor = val contentColor =
if (state.isShielded) { if (state.isShielded) {
ZashiColors.Utility.Purple.utilityPurple800 ZashiColors.Utility.Purple.utilityPurple800 orDark ZashiColors.Utility.Indigo.utilityIndigo800
} else { } else {
ZashiColors.Text.textPrimary ZashiColors.Text.textPrimary
} }
@ -272,16 +264,14 @@ private fun ReceiveIconButton(
.background(containerColor, RoundedCornerShape(ZashiDimensions.Radius.radiusXl)) .background(containerColor, RoundedCornerShape(ZashiDimensions.Radius.radiusXl))
.clip(RoundedCornerShape(ZashiDimensions.Radius.radiusXl)) .clip(RoundedCornerShape(ZashiDimensions.Radius.radiusXl))
.clickable { onClick() } .clickable { onClick() }
.padding(ZcashTheme.dimens.spacingMid) .padding(12.dp)
) { ) {
Icon( Icon(
painter = iconPainter, painter = iconPainter,
contentDescription = text, contentDescription = text,
tint = contentColor tint = contentColor
) )
Spacer(4.dp)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
Text( Text(
text = text, text = text,
color = contentColor, color = contentColor,

View File

@ -78,12 +78,7 @@ class ReceiveViewModel(
icon = icon =
when (account) { when (account) {
is KeystoneAccount -> co.electriccoin.zcash.ui.design.R.drawable.ic_item_keystone is KeystoneAccount -> co.electriccoin.zcash.ui.design.R.drawable.ic_item_keystone
is ZashiAccount -> is ZashiAccount -> R.drawable.ic_zec_round_full
if (type == ReceiveAddressType.Unified) {
R.drawable.ic_zec_round_full
} else {
R.drawable.ic_zec_round_stroke
}
}, },
title = title =
when (account) { when (account) {

View File

@ -12,6 +12,7 @@ import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.repository.Transaction import co.electriccoin.zcash.ui.common.repository.Transaction
import co.electriccoin.zcash.ui.common.usecase.GetTransactionsUseCase import co.electriccoin.zcash.ui.common.usecase.GetTransactionsUseCase
import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToRequestShieldedUseCase
import co.electriccoin.zcash.ui.design.component.ButtonState import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType
@ -21,6 +22,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class TransactionHistoryWidgetViewModel( class TransactionHistoryWidgetViewModel(
getTransactions: GetTransactionsUseCase, getTransactions: GetTransactionsUseCase,
@ -28,6 +30,7 @@ class TransactionHistoryWidgetViewModel(
private val transactionHistoryMapper: TransactionHistoryMapper, private val transactionHistoryMapper: TransactionHistoryMapper,
private val navigationRouter: NavigationRouter, private val navigationRouter: NavigationRouter,
private val restoreTimestampDataSource: RestoreTimestampDataSource, private val restoreTimestampDataSource: RestoreTimestampDataSource,
private val navigateToRequestShielded: NavigateToRequestShieldedUseCase
) : ViewModel() { ) : ViewModel() {
val state = val state =
combine( combine(
@ -88,9 +91,7 @@ class TransactionHistoryWidgetViewModel(
navigationRouter.forward(TransactionHistory) navigationRouter.forward(TransactionHistory)
} }
private fun onRequestZecClick() { private fun onRequestZecClick() = viewModelScope.launch { navigateToRequestShielded() }
navigationRouter.forward("${NavigationTargets.REQUEST}/${ReceiveAddressType.Unified.ordinal}")
}
} }
private const val MAX_TRANSACTION_COUNT = 5 private const val MAX_TRANSACTION_COUNT = 5