Shielded address rotation
This commit is contained in:
parent
52a5fd53ff
commit
72534e42f4
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue