diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiButton.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiButton.kt index 490f43f18..f22d6d5b9 100644 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiButton.kt +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiButton.kt @@ -203,7 +203,7 @@ object ZashiButtonDefaults { contentColor: Color = source.Btns.Secondary.btnSecondaryFg, borderColor: Color = Color.Unspecified, disabledContainerColor: Color = source.Btns.Secondary.btnSecondaryBgDisabled, - disabledContentColor: Color = source.Btns.Secondary.btnSecondaryFg, + disabledContentColor: Color = source.Btns.Secondary.btnSecondaryFgDisabled, ) = ZashiButtonColors( containerColor = containerColor, contentColor = contentColor, diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiScreenModalBottomSheet.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiScreenModalBottomSheet.kt index 449b96ce7..879798e57 100644 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiScreenModalBottomSheet.kt +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiScreenModalBottomSheet.kt @@ -2,6 +2,7 @@ package co.electriccoin.zcash.ui.design.component import android.view.WindowManager import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsBottomHeight @@ -27,7 +28,7 @@ import co.electriccoin.zcash.ui.design.LocalSheetStateManager fun ZashiScreenModalBottomSheet( state: T?, sheetState: SheetState = rememberScreenModalBottomSheetState(), - content: @Composable (state: T) -> Unit = {}, + content: @Composable ColumnScope.(state: T) -> Unit = {}, ) { val parent = LocalView.current.parent SideEffect { diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/DateSourceModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/DateSourceModule.kt index b598bdfc5..30c406cc6 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/DateSourceModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/DateSourceModule.kt @@ -2,10 +2,14 @@ package co.electriccoin.zcash.di import co.electriccoin.zcash.ui.common.datasource.AccountDataSource import co.electriccoin.zcash.ui.common.datasource.AccountDataSourceImpl +import co.electriccoin.zcash.ui.common.datasource.MessageAvailabilityDataSource +import co.electriccoin.zcash.ui.common.datasource.MessageAvailabilityDataSourceImpl import co.electriccoin.zcash.ui.common.datasource.ProposalDataSource import co.electriccoin.zcash.ui.common.datasource.ProposalDataSourceImpl import co.electriccoin.zcash.ui.common.datasource.RestoreTimestampDataSource import co.electriccoin.zcash.ui.common.datasource.RestoreTimestampDataSourceImpl +import co.electriccoin.zcash.ui.common.datasource.ShieldFundsDataSource +import co.electriccoin.zcash.ui.common.datasource.ShieldFundsDataSourceImpl import co.electriccoin.zcash.ui.common.datasource.WalletBackupDataSource import co.electriccoin.zcash.ui.common.datasource.WalletBackupDataSourceImpl import co.electriccoin.zcash.ui.common.datasource.ZashiSpendingKeyDataSource @@ -21,4 +25,6 @@ val dataSourceModule = singleOf(::ProposalDataSourceImpl) bind ProposalDataSource::class singleOf(::RestoreTimestampDataSourceImpl) bind RestoreTimestampDataSource::class singleOf(::WalletBackupDataSourceImpl) bind WalletBackupDataSource::class + singleOf(::ShieldFundsDataSourceImpl) bind ShieldFundsDataSource::class + singleOf(::MessageAvailabilityDataSourceImpl) bind MessageAvailabilityDataSource::class } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/ProviderModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/ProviderModule.kt index 84de0cf8d..f3c4493f7 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/ProviderModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/ProviderModule.kt @@ -18,6 +18,8 @@ import co.electriccoin.zcash.ui.common.provider.ShieldFundsRemindMeTimestampStor import co.electriccoin.zcash.ui.common.provider.ShieldFundsRemindMeTimestampStorageProviderImpl import co.electriccoin.zcash.ui.common.provider.SynchronizerProvider import co.electriccoin.zcash.ui.common.provider.SynchronizerProviderImpl +import co.electriccoin.zcash.ui.common.provider.WalletBackupConsentStorageProvider +import co.electriccoin.zcash.ui.common.provider.WalletBackupConsentStorageProviderImpl import co.electriccoin.zcash.ui.common.provider.WalletBackupFlagStorageProvider import co.electriccoin.zcash.ui.common.provider.WalletBackupFlagStorageProviderImpl import co.electriccoin.zcash.ui.common.provider.WalletBackupRemindMeCountStorageProvider @@ -49,4 +51,5 @@ val providerModule = factoryOf(::WalletBackupRemindMeTimestampStorageProviderImpl) bind WalletBackupRemindMeTimestampStorageProvider::class factoryOf(::WalletBackupFlagStorageProviderImpl) bind WalletBackupFlagStorageProvider::class + factoryOf(::WalletBackupConsentStorageProviderImpl) bind WalletBackupConsentStorageProvider::class } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/RepositoryModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/RepositoryModule.kt index a1bcf4d0a..16c67d25a 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/RepositoryModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/RepositoryModule.kt @@ -10,6 +10,8 @@ import co.electriccoin.zcash.ui.common.repository.FlexaRepository import co.electriccoin.zcash.ui.common.repository.FlexaRepositoryImpl import co.electriccoin.zcash.ui.common.repository.KeystoneProposalRepository import co.electriccoin.zcash.ui.common.repository.KeystoneProposalRepositoryImpl +import co.electriccoin.zcash.ui.common.repository.ShieldFundsRepository +import co.electriccoin.zcash.ui.common.repository.ShieldFundsRepositoryImpl import co.electriccoin.zcash.ui.common.repository.TransactionFilterRepository import co.electriccoin.zcash.ui.common.repository.TransactionFilterRepositoryImpl import co.electriccoin.zcash.ui.common.repository.TransactionRepository @@ -33,4 +35,5 @@ val repositoryModule = singleOf(::TransactionRepositoryImpl) bind TransactionRepository::class singleOf(::TransactionFilterRepositoryImpl) bind TransactionFilterRepository::class singleOf(::ZashiProposalRepositoryImpl) bind ZashiProposalRepository::class + singleOf(::ShieldFundsRepositoryImpl) bind ShieldFundsRepository::class } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt index 2b5860f84..c4e58f03b 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt @@ -8,7 +8,6 @@ import co.electriccoin.zcash.ui.common.usecase.CopyToClipboardUseCase import co.electriccoin.zcash.ui.common.usecase.CreateFlexaTransactionUseCase import co.electriccoin.zcash.ui.common.usecase.CreateKeystoneAccountUseCase import co.electriccoin.zcash.ui.common.usecase.CreateKeystoneProposalPCZTEncoderUseCase -import co.electriccoin.zcash.ui.common.usecase.CreateKeystoneShieldProposalUseCase import co.electriccoin.zcash.ui.common.usecase.CreateOrUpdateTransactionNoteUseCase import co.electriccoin.zcash.ui.common.usecase.CreateProposalUseCase import co.electriccoin.zcash.ui.common.usecase.DeleteContactUseCase @@ -39,21 +38,19 @@ import co.electriccoin.zcash.ui.common.usecase.GetTransparentAddressUseCase import co.electriccoin.zcash.ui.common.usecase.GetWalletAccountsUseCase import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase import co.electriccoin.zcash.ui.common.usecase.GetZashiAccountUseCase -import co.electriccoin.zcash.ui.common.usecase.GetZashiSpendingKeyUseCase import co.electriccoin.zcash.ui.common.usecase.IsCoinbaseAvailableUseCase import co.electriccoin.zcash.ui.common.usecase.IsFlexaAvailableUseCase import co.electriccoin.zcash.ui.common.usecase.IsRestoreSuccessDialogVisibleUseCase import co.electriccoin.zcash.ui.common.usecase.MarkTxMemoAsReadUseCase import co.electriccoin.zcash.ui.common.usecase.NavigateToAddressBookUseCase import co.electriccoin.zcash.ui.common.usecase.NavigateToCoinbaseUseCase -import co.electriccoin.zcash.ui.common.usecase.NavigateToWalletBackupUseCase import co.electriccoin.zcash.ui.common.usecase.NavigateToTaxExportUseCase +import co.electriccoin.zcash.ui.common.usecase.NavigateToWalletBackupUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveAddressBookContactsUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveClearSendUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveContactByAddressUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveContactPickedUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveFastestServersUseCase -import co.electriccoin.zcash.ui.common.usecase.ObserveOnAccountChangedUseCase import co.electriccoin.zcash.ui.common.usecase.ObservePersistableWalletUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveProposalUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedEndpointUseCase @@ -70,6 +67,7 @@ import co.electriccoin.zcash.ui.common.usecase.ParseKeystoneUrToZashiAccountsUse import co.electriccoin.zcash.ui.common.usecase.PersistEndpointUseCase import co.electriccoin.zcash.ui.common.usecase.PrefillSendUseCase import co.electriccoin.zcash.ui.common.usecase.RefreshFastestServersUseCase +import co.electriccoin.zcash.ui.common.usecase.RemindShieldFundsLaterUseCase import co.electriccoin.zcash.ui.common.usecase.RemindWalletBackupLaterUseCase import co.electriccoin.zcash.ui.common.usecase.RescanBlockchainUseCase import co.electriccoin.zcash.ui.common.usecase.ResetInMemoryDataUseCase @@ -83,6 +81,7 @@ import co.electriccoin.zcash.ui.common.usecase.SendSupportEmailUseCase import co.electriccoin.zcash.ui.common.usecase.SendTransactionAgainUseCase import co.electriccoin.zcash.ui.common.usecase.ShareImageUseCase import co.electriccoin.zcash.ui.common.usecase.SharePCZTUseCase +import co.electriccoin.zcash.ui.common.usecase.ShieldFundsUseCase import co.electriccoin.zcash.ui.common.usecase.UpdateContactUseCase import co.electriccoin.zcash.ui.common.usecase.ValidateContactAddressUseCase import co.electriccoin.zcash.ui.common.usecase.ValidateContactNameUseCase @@ -125,7 +124,6 @@ val useCaseModule = factoryOf(::Zip321BuildUriUseCase) factoryOf(::Zip321ParseUriValidationUseCase) factoryOf(::IsCoinbaseAvailableUseCase) - factoryOf(::GetZashiSpendingKeyUseCase) factoryOf(::ObservePersistableWalletUseCase) factoryOf(::GetSupportUseCase) factoryOf(::SendEmailUseCase) @@ -148,14 +146,12 @@ val useCaseModule = factoryOf(::CreateProposalUseCase) factoryOf(::OnZip321ScannedUseCase) factoryOf(::OnAddressScannedUseCase) - factoryOf(::CreateKeystoneShieldProposalUseCase) factoryOf(::ParseKeystonePCZTUseCase) factoryOf(::ParseKeystoneSignInRequestUseCase) factoryOf(::CancelProposalFlowUseCase) factoryOf(::ObserveProposalUseCase) factoryOf(::SharePCZTUseCase) factoryOf(::CreateKeystoneProposalPCZTEncoderUseCase) - factoryOf(::ObserveOnAccountChangedUseCase) factoryOf(::ViewTransactionsAfterSuccessfulProposalUseCase) factoryOf(::ViewTransactionDetailAfterSuccessfulProposalUseCase) factoryOf(::NavigateToCoinbaseUseCase) @@ -192,4 +188,6 @@ val useCaseModule = factoryOf(::GetHomeMessageUseCase) factoryOf(::OnUserSavedWalletBackupUseCase) factoryOf(::RemindWalletBackupLaterUseCase) + factoryOf(::RemindShieldFundsLaterUseCase) + singleOf(::ShieldFundsUseCase) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt index bb0619909..d24172be0 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt @@ -19,7 +19,7 @@ import co.electriccoin.zcash.ui.screen.flexa.FlexaViewModel import co.electriccoin.zcash.ui.screen.home.HomeViewModel import co.electriccoin.zcash.ui.screen.home.backup.WalletBackupDetailViewModel import co.electriccoin.zcash.ui.screen.home.backup.WalletBackupInfoViewModel -import co.electriccoin.zcash.ui.screen.home.transparentbalance.TransparentBalanceInfoViewModel +import co.electriccoin.zcash.ui.screen.home.shieldfunds.ShieldFundsInfoViewModel import co.electriccoin.zcash.ui.screen.integrations.IntegrationsViewModel import co.electriccoin.zcash.ui.screen.qrcode.viewmodel.QrCodeViewModel import co.electriccoin.zcash.ui.screen.receive.viewmodel.ReceiveViewModel @@ -38,7 +38,6 @@ import co.electriccoin.zcash.ui.screen.walletbackup.WalletBackupViewModel import co.electriccoin.zcash.ui.screen.selectkeystoneaccount.SelectKeystoneAccount import co.electriccoin.zcash.ui.screen.selectkeystoneaccount.viewmodel.SelectKeystoneAccountViewModel import co.electriccoin.zcash.ui.screen.send.SendViewModel -import co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel.CreateTransactionsViewModel import co.electriccoin.zcash.ui.screen.settings.viewmodel.ScreenBrightnessViewModel import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel import co.electriccoin.zcash.ui.screen.signkeystonetransaction.viewmodel.SignKeystoneTransactionViewModel @@ -69,7 +68,6 @@ val viewModelModule = viewModelOf(::SettingsViewModel) viewModelOf(::AdvancedSettingsViewModel) viewModelOf(::SupportViewModel) - viewModelOf(::CreateTransactionsViewModel) viewModelOf(::RestoreSuccessViewModel) viewModelOf(::WhatsNewViewModel) viewModelOf(::ChooseServerViewModel) @@ -147,7 +145,7 @@ val viewModelModule = viewModelOf(::RestoreBDHeightViewModel) viewModelOf(::RestoreBDDateViewModel) viewModelOf(::RestoreBDEstimationViewModel) - viewModelOf(::TransparentBalanceInfoViewModel) + viewModelOf(::ShieldFundsInfoViewModel) viewModelOf(::WalletBackupInfoViewModel) viewModelOf(::ExchangeRateOptInViewModel) viewModelOf(::ExchangeRateSettingsViewModel) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt index 76469241a..92fe2cbe7 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt @@ -78,8 +78,8 @@ import co.electriccoin.zcash.ui.screen.home.disconnected.WalletDisconnectedInfo import co.electriccoin.zcash.ui.screen.home.restoring.WalletRestoringInfo import co.electriccoin.zcash.ui.screen.home.syncing.WalletSyncingInfo import co.electriccoin.zcash.ui.screen.home.updating.WalletUpdatingInfo -import co.electriccoin.zcash.ui.screen.home.transparentbalance.AndroidTransparentBalanceInfo -import co.electriccoin.zcash.ui.screen.home.transparentbalance.TransparentBalanceInfo +import co.electriccoin.zcash.ui.screen.home.shieldfunds.AndroidShieldFundsInfo +import co.electriccoin.zcash.ui.screen.home.shieldfunds.ShieldFundsInfo import co.electriccoin.zcash.ui.screen.integrations.AndroidDialogIntegrations import co.electriccoin.zcash.ui.screen.integrations.AndroidIntegrations import co.electriccoin.zcash.ui.screen.integrations.DialogIntegrations @@ -422,14 +422,14 @@ internal fun MainActivity.Navigation() { ) { AndroidWalletBackupInfo() } - dialog( + dialog( dialogProperties = DialogProperties( dismissOnBackPress = false, dismissOnClickOutside = false ) ) { - AndroidTransparentBalanceInfo() + AndroidShieldFundsInfo() } dialog( dialogProperties = diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/AccountDataSource.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/AccountDataSource.kt index 1032e1907..ad004bf0d 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/AccountDataSource.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/AccountDataSource.kt @@ -24,7 +24,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -37,13 +36,10 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.retryWhen import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.time.Duration.Companion.seconds interface AccountDataSource { - val onAccountChanged: Flow - val allAccounts: StateFlow?> val selectedAccount: Flow @@ -74,8 +70,6 @@ class AccountDataSourceImpl( ) : AccountDataSource { private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - override val onAccountChanged = MutableSharedFlow() - @OptIn(ExperimentalCoroutinesApi::class) private val internalAccounts: Flow?> = synchronizerProvider @@ -224,15 +218,7 @@ class AccountDataSourceImpl( override suspend fun selectAccount(account: Account) { withContext(Dispatchers.IO) { - val current = selectedAccountUUIDProvider.getUUID() - selectedAccountUUIDProvider.setUUID(account.accountUuid) - - scope.launch { - if (current != account.accountUuid) { - onAccountChanged.emit(Unit) - } - } } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/MessageAvailabilityDataSource.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/MessageAvailabilityDataSource.kt new file mode 100644 index 000000000..433deed7b --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/MessageAvailabilityDataSource.kt @@ -0,0 +1,46 @@ +package co.electriccoin.zcash.ui.common.datasource + +import androidx.lifecycle.Lifecycle +import co.electriccoin.zcash.ui.common.provider.ApplicationStateProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +interface MessageAvailabilityDataSource { + val canShowMessage: Boolean + fun observe(): StateFlow + fun onMessageShown() +} + +class MessageAvailabilityDataSourceImpl( + private val applicationStateProvider: ApplicationStateProvider +): MessageAvailabilityDataSource { + + private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + + private val state = MutableStateFlow(true) + + override val canShowMessage: Boolean + get() = state.value + + init { + scope.launch { + applicationStateProvider.state.collect { + if (it == Lifecycle.Event.ON_START) { + state.update { true } + } + } + } + } + + override fun observe(): StateFlow = state.asStateFlow() + + override fun onMessageShown() { + state.update { false } + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/ShieldFundsDataSource.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/ShieldFundsDataSource.kt new file mode 100644 index 000000000..7cc9ddaa1 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/ShieldFundsDataSource.kt @@ -0,0 +1,93 @@ +package co.electriccoin.zcash.ui.common.datasource + +import androidx.annotation.StringRes +import cash.z.ecc.android.sdk.model.AccountUuid +import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.common.provider.ShieldFundsRemindMeCountStorageProvider +import co.electriccoin.zcash.ui.common.provider.ShieldFundsRemindMeTimestampStorageProvider +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import java.time.Instant +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +interface ShieldFundsDataSource { + suspend fun observe(forAccount: AccountUuid): Flow + + suspend fun remindMeLater(forAccount: AccountUuid) +} + +class ShieldFundsDataSourceImpl( + private val shieldFundsRemindMeCountStorageProvider: ShieldFundsRemindMeCountStorageProvider, + private val shieldFundsRemindMeTimestampStorageProvider: ShieldFundsRemindMeTimestampStorageProvider +): ShieldFundsDataSource { + + @OptIn(ExperimentalCoroutinesApi::class) + override suspend fun observe(forAccount: AccountUuid): Flow = combine( + shieldFundsRemindMeCountStorageProvider.observe(forAccount), + shieldFundsRemindMeTimestampStorageProvider.observe(forAccount) + ) { count, timestamp -> + count to timestamp + }.flatMapLatest {(count, timestamp) -> + when { + timestamp == null -> flowOf(ShieldFundsAvailability.Available(ShieldFundsLockoutDuration.TWO_DAYS)) + count == 1 -> calculateNext( + lastTimestamp = timestamp, + lastLockoutDuration = ShieldFundsLockoutDuration.TWO_DAYS, + nextLockoutDuration = ShieldFundsLockoutDuration.TWO_WEEKS + ) + + else -> calculateNext( + lastTimestamp = timestamp, + lastLockoutDuration = if (count == 2) { + ShieldFundsLockoutDuration.TWO_WEEKS + } else { + ShieldFundsLockoutDuration.ONE_MONTH + }, + nextLockoutDuration = ShieldFundsLockoutDuration.ONE_MONTH + ) + } + } + + override suspend fun remindMeLater(forAccount: AccountUuid) { + val count = shieldFundsRemindMeCountStorageProvider.get(forAccount) + val timestamp = Instant.now() + shieldFundsRemindMeCountStorageProvider.store(forAccount, count + 1) + shieldFundsRemindMeTimestampStorageProvider.store(forAccount, timestamp) + } + + private fun calculateNext( + lastTimestamp: Instant, + lastLockoutDuration: ShieldFundsLockoutDuration, + nextLockoutDuration: ShieldFundsLockoutDuration + ): Flow { + val nextAvailableTimestamp = lastTimestamp.plusMillis(lastLockoutDuration.duration.inWholeMilliseconds) + val now = Instant.now() + return if (nextAvailableTimestamp > now) { + flow { + val remaining = nextAvailableTimestamp.toEpochMilli() - now.toEpochMilli() + emit(ShieldFundsAvailability.Unavailable) + delay(remaining) + emit(ShieldFundsAvailability.Available(nextLockoutDuration)) + } + } else { + flowOf(ShieldFundsAvailability.Available(nextLockoutDuration)) + } + } +} + +sealed interface ShieldFundsAvailability { + data class Available(val lockoutDuration: ShieldFundsLockoutDuration) : ShieldFundsAvailability + data object Unavailable : ShieldFundsAvailability +} + +enum class ShieldFundsLockoutDuration(val duration: Duration, @StringRes val res: Int) { + TWO_DAYS(10.seconds, R.string.general_remind_me_in_two_days), + TWO_WEEKS(20.seconds, R.string.general_remind_me_in_two_weeks), + ONE_MONTH(30.seconds, R.string.general_remind_me_in_two_months) +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/ShieldFundsRemindMeDataSource.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/ShieldFundsRemindMeDataSource.kt deleted file mode 100644 index cd1b0a667..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/ShieldFundsRemindMeDataSource.kt +++ /dev/null @@ -1,35 +0,0 @@ -package co.electriccoin.zcash.ui.common.datasource - -import cash.z.ecc.android.sdk.model.AccountUuid -import co.electriccoin.zcash.ui.common.provider.ShieldFundsRemindMeCountStorageProvider -import co.electriccoin.zcash.ui.common.provider.ShieldFundsRemindMeTimestampStorageProvider -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlin.time.Duration -import kotlin.time.Duration.Companion.days - -interface ShieldFundsRemindMeDataSource { - suspend fun observe(forAccount: AccountUuid): Flow -} - -class ShieldFundsRemindMeDataSourceImpl( - private val shieldFundsRemindMeCountStorageProvider: ShieldFundsRemindMeCountStorageProvider, - private val shieldFundsRemindMeTimestampStorageProvider: ShieldFundsRemindMeTimestampStorageProvider -): ShieldFundsRemindMeDataSource { - override suspend fun observe(forAccount: AccountUuid): Flow = combine( - shieldFundsRemindMeCountStorageProvider.observe(forAccount), - shieldFundsRemindMeTimestampStorageProvider.observe(forAccount) - ) { count, timestamp -> - when { - timestamp == null -> ShieldFundsAvailability.Available(1.days) - count == 1 -> ShieldFundsAvailability.Available(2.days) - count == 2 -> ShieldFundsAvailability.Available(3.days) - else -> ShieldFundsAvailability.Unavailable - } - } -} - -sealed interface ShieldFundsAvailability { - data class Available(val nextLockoutDuration: Duration) : ShieldFundsAvailability - data object Unavailable : ShieldFundsAvailability -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/WalletBackupDataSource.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/WalletBackupDataSource.kt index ea2f0832c..e88b4f12e 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/WalletBackupDataSource.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/datasource/WalletBackupDataSource.kt @@ -1,5 +1,7 @@ package co.electriccoin.zcash.ui.common.datasource +import androidx.annotation.StringRes +import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.common.provider.WalletBackupFlagStorageProvider import co.electriccoin.zcash.ui.common.provider.WalletBackupRemindMeCountStorageProvider import co.electriccoin.zcash.ui.common.provider.WalletBackupRemindMeTimestampStorageProvider @@ -38,21 +40,21 @@ class WalletBackupDataSourceImpl( }.flatMapLatest { (isBackedUp, count, timestamp) -> when { isBackedUp -> flowOf(WalletBackupAvailability.Unavailable) - timestamp == null -> flowOf(WalletBackupAvailability.Available(WalletBackupLockoutDuration.ONE_DAY)) + timestamp == null -> flowOf(WalletBackupAvailability.Available(WalletBackupLockoutDuration.TWO_DAYS)) count == 1 -> calculateNext( lastTimestamp = timestamp, - lastLockoutDuration = WalletBackupLockoutDuration.ONE_DAY, - nextLockoutDuration = WalletBackupLockoutDuration.TWO_DAYS + lastLockoutDuration = WalletBackupLockoutDuration.TWO_DAYS, + nextLockoutDuration = WalletBackupLockoutDuration.TWO_WEEKS ) else -> calculateNext( lastTimestamp = timestamp, lastLockoutDuration = if (count == 2) { - WalletBackupLockoutDuration.TWO_DAYS + WalletBackupLockoutDuration.TWO_WEEKS } else { - WalletBackupLockoutDuration.THREE_DAYS + WalletBackupLockoutDuration.ONE_MONTH }, - nextLockoutDuration = WalletBackupLockoutDuration.THREE_DAYS + nextLockoutDuration = WalletBackupLockoutDuration.ONE_MONTH ) } } @@ -93,8 +95,8 @@ sealed interface WalletBackupAvailability { data object Unavailable : WalletBackupAvailability } -enum class WalletBackupLockoutDuration(val duration: Duration) { - ONE_DAY(10.seconds), - TWO_DAYS(20.seconds), - THREE_DAYS(30.seconds) +enum class WalletBackupLockoutDuration(val duration: Duration, @StringRes val res: Int) { + TWO_DAYS(10.seconds, R.string.general_remind_me_in_two_days), + TWO_WEEKS(20.seconds, R.string.general_remind_me_in_two_weeks), + ONE_MONTH(30.seconds, R.string.general_remind_me_in_two_months), } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/provider/WalletBackupConsentStorageProvider.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/provider/WalletBackupConsentStorageProvider.kt new file mode 100644 index 000000000..0a5ba202d --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/provider/WalletBackupConsentStorageProvider.kt @@ -0,0 +1,11 @@ +package co.electriccoin.zcash.ui.common.provider + +import co.electriccoin.zcash.preference.EncryptedPreferenceProvider +import co.electriccoin.zcash.preference.model.entry.PreferenceKey + +interface WalletBackupConsentStorageProvider : BooleanStorageProvider + +class WalletBackupConsentStorageProviderImpl( + override val preferenceHolder: EncryptedPreferenceProvider +) : BaseBooleanStorageProvider(key = PreferenceKey("wallet_backup_consent")), + WalletBackupConsentStorageProvider diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/ShieldFundsRepository.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/ShieldFundsRepository.kt new file mode 100644 index 000000000..5e3f64e41 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/repository/ShieldFundsRepository.kt @@ -0,0 +1,65 @@ +package co.electriccoin.zcash.ui.common.repository + +import cash.z.ecc.android.sdk.model.Zatoshi +import co.electriccoin.zcash.ui.common.datasource.AccountDataSource +import co.electriccoin.zcash.ui.common.datasource.ShieldFundsAvailability +import co.electriccoin.zcash.ui.common.datasource.ShieldFundsDataSource +import co.electriccoin.zcash.ui.common.datasource.ShieldFundsLockoutDuration +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +interface ShieldFundsRepository { + val availability: Flow + + suspend fun remindMeLater() +} + +class ShieldFundsRepositoryImpl( + private val accountDataSource: AccountDataSource, + private val shieldFundsDataSource: ShieldFundsDataSource, +) : ShieldFundsRepository { + @OptIn(ExperimentalCoroutinesApi::class) + override val availability: Flow = accountDataSource + .selectedAccount + .flatMapLatest { account -> + when { + account == null -> + flowOf(ShieldFundsData.Unavailable) + + account.transparent.balance >= Zatoshi(DEFAULT_SHIELDING_THRESHOLD) -> + shieldFundsDataSource.observe(account.sdkAccount.accountUuid).map { + when (it) { + is ShieldFundsAvailability.Available -> ShieldFundsData.Available( + lockoutDuration = it.lockoutDuration, + amount = account.transparent.balance + ) + + ShieldFundsAvailability.Unavailable -> ShieldFundsData.Unavailable + } + } + + else -> flowOf(ShieldFundsData.Unavailable) + } + } + + override suspend fun remindMeLater() { + shieldFundsDataSource.remindMeLater( + forAccount = accountDataSource.getSelectedAccount().sdkAccount.accountUuid + ) + } +} + +sealed interface ShieldFundsData { + data class Available( + val lockoutDuration: ShieldFundsLockoutDuration, + val amount: Zatoshi + ) : ShieldFundsData + + data object Unavailable : ShieldFundsData +} + +private const val DEFAULT_SHIELDING_THRESHOLD = 100000L + diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/CreateFlexaTransactionUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/CreateFlexaTransactionUseCase.kt index 896b19009..64460f2d3 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/CreateFlexaTransactionUseCase.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/CreateFlexaTransactionUseCase.kt @@ -13,6 +13,7 @@ import cash.z.ecc.android.sdk.model.proposeSend import cash.z.ecc.android.sdk.type.AddressType import co.electriccoin.zcash.spackle.Twig import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.common.datasource.ZashiSpendingKeyDataSource import co.electriccoin.zcash.ui.common.model.SubmitResult import co.electriccoin.zcash.ui.common.repository.BiometricRepository import co.electriccoin.zcash.ui.common.repository.BiometricRequest @@ -25,7 +26,7 @@ import com.flexa.spend.buildSpend class CreateFlexaTransactionUseCase( private val getSynchronizer: GetSynchronizerUseCase, private val getZashiAccount: GetZashiAccountUseCase, - private val getSpendingKey: GetZashiSpendingKeyUseCase, + private val zashiSpendingKeyDataSource: ZashiSpendingKeyDataSource, private val biometricRepository: BiometricRepository, private val context: Context, ) { @@ -42,7 +43,10 @@ class CreateFlexaTransactionUseCase( ) }.onSuccess { proposal -> Twig.debug { "Transaction proposal successful: ${proposal.toPrettyString()}" } - val result = submitTransactions(proposal = proposal, spendingKey = getSpendingKey()) + val result = submitTransactions( + proposal = proposal, + spendingKey = zashiSpendingKeyDataSource.getZashiSpendingKey() + ) when (val output = result.first) { is SubmitResult.Success -> { Twig.debug { "Transaction successful $result" } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/CreateKeystoneShieldProposalUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/CreateKeystoneShieldProposalUseCase.kt deleted file mode 100644 index 014e8b7a1..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/CreateKeystoneShieldProposalUseCase.kt +++ /dev/null @@ -1,22 +0,0 @@ -package co.electriccoin.zcash.ui.common.usecase - -import co.electriccoin.zcash.ui.NavigationRouter -import co.electriccoin.zcash.ui.common.repository.KeystoneProposalRepository -import co.electriccoin.zcash.ui.screen.signkeystonetransaction.SignKeystoneTransaction - -class CreateKeystoneShieldProposalUseCase( - private val keystoneProposalRepository: KeystoneProposalRepository, - private val navigationRouter: NavigationRouter -) { - @Suppress("TooGenericExceptionCaught") - suspend operator fun invoke() { - try { - keystoneProposalRepository.createShieldProposal() - keystoneProposalRepository.createPCZTFromProposal() - navigationRouter.forward(SignKeystoneTransaction) - } catch (e: Exception) { - keystoneProposalRepository.clear() - throw e - } - } -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetHomeMessageUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetHomeMessageUseCase.kt index 25ba8ce3c..81df292e3 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetHomeMessageUseCase.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetHomeMessageUseCase.kt @@ -6,19 +6,17 @@ import co.electriccoin.zcash.ui.common.datasource.WalletBackupAvailability import co.electriccoin.zcash.ui.common.datasource.WalletBackupDataSource import co.electriccoin.zcash.ui.common.model.WalletRestoringState import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository +import co.electriccoin.zcash.ui.common.repository.ShieldFundsData +import co.electriccoin.zcash.ui.common.repository.ShieldFundsRepository import co.electriccoin.zcash.ui.common.repository.WalletRepository import co.electriccoin.zcash.ui.common.viewmodel.SynchronizerError import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState -import co.electriccoin.zcash.ui.util.Quadruple -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlin.time.Duration.Companion.seconds @@ -26,23 +24,23 @@ class GetHomeMessageUseCase( private val walletRepository: WalletRepository, private val walletBackupDataSource: WalletBackupDataSource, private val exchangeRateRepository: ExchangeRateRepository, + private val shieldFundsRepository: ShieldFundsRepository, ) { - @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) - fun observe() = combine( + @OptIn(FlowPreview::class) + fun observe(): Flow = combine( walletRepository.currentWalletSnapshot.filterNotNull(), walletRepository.walletRestoringState, walletBackupDataSource.observe(), exchangeRateRepository.state.map { it == ExchangeRateState.OptIn }.distinctUntilChanged(), - ) { walletSnapshot, walletStateInformation, backup, isCCAvailable -> - Quadruple(walletSnapshot, walletStateInformation, backup, isCCAvailable) - }.flatMapLatest { (walletSnapshot, walletStateInformation, backup, isCCAvailable) -> + shieldFundsRepository.availability + ) { walletSnapshot, walletStateInformation, backup, isCCAvailable, shieldFunds -> when { walletSnapshot.synchronizerError != null -> { - flowOf(HomeMessageData.Error(walletSnapshot.synchronizerError)) + HomeMessageData.Error(walletSnapshot.synchronizerError) } walletSnapshot.status == Synchronizer.Status.DISCONNECTED -> { - flowOf(HomeMessageData.Disconnected) + HomeMessageData.Disconnected } walletSnapshot.status in listOf( @@ -50,34 +48,35 @@ class GetHomeMessageUseCase( Synchronizer.Status.SYNCING, Synchronizer.Status.STOPPED ) -> { - flow { - val progress = walletSnapshot.progress.decimal * 100f - val result = when { - walletStateInformation == WalletRestoringState.RESTORING -> { - HomeMessageData.Restoring( - progress = progress, - ) - } - - else -> { - HomeMessageData.Syncing(progress = progress) - } + val progress = walletSnapshot.progress.decimal * 100f + val result = when { + walletStateInformation == WalletRestoringState.RESTORING -> { + HomeMessageData.Restoring( + progress = progress, + ) + } + + else -> { + HomeMessageData.Syncing(progress = progress) } - emit(result) } + result } - backup is WalletBackupAvailability.Available -> flowOf(HomeMessageData.Backup) - isCCAvailable -> flowOf(HomeMessageData.EnableCurrencyConversion) + shieldFunds is ShieldFundsData.Available -> HomeMessageData.ShieldFunds(shieldFunds.amount) - else -> flowOf(null) + backup is WalletBackupAvailability.Available -> HomeMessageData.Backup + + isCCAvailable -> HomeMessageData.EnableCurrencyConversion + + else -> null } }.debounce(.5.seconds) } sealed interface HomeMessageData { data object EnableCurrencyConversion : HomeMessageData - data class TransparentBalance(val zatoshi: Zatoshi) : HomeMessageData + data class ShieldFunds(val zatoshi: Zatoshi) : HomeMessageData data object Backup : HomeMessageData data object Disconnected : HomeMessageData data class Error(val synchronizerError: SynchronizerError) : HomeMessageData diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetZashiSpendingKeyUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetZashiSpendingKeyUseCase.kt deleted file mode 100644 index 29a0090cb..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetZashiSpendingKeyUseCase.kt +++ /dev/null @@ -1,9 +0,0 @@ -package co.electriccoin.zcash.ui.common.usecase - -import co.electriccoin.zcash.ui.common.datasource.ZashiSpendingKeyDataSource - -class GetZashiSpendingKeyUseCase( - private val spendingKeyDataSource: ZashiSpendingKeyDataSource, -) { - suspend operator fun invoke() = spendingKeyDataSource.getZashiSpendingKey() -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/ObserveOnAccountChangedUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/ObserveOnAccountChangedUseCase.kt deleted file mode 100644 index 9854e009d..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/ObserveOnAccountChangedUseCase.kt +++ /dev/null @@ -1,9 +0,0 @@ -package co.electriccoin.zcash.ui.common.usecase - -import co.electriccoin.zcash.ui.common.datasource.AccountDataSource - -class ObserveOnAccountChangedUseCase( - private val accountDataSource: AccountDataSource -) { - operator fun invoke() = accountDataSource.onAccountChanged -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/OnUserSavedWalletBackupUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/OnUserSavedWalletBackupUseCase.kt index eb8799aff..1ad14635f 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/OnUserSavedWalletBackupUseCase.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/OnUserSavedWalletBackupUseCase.kt @@ -8,7 +8,7 @@ class OnUserSavedWalletBackupUseCase( private val walletBackupDataSource: WalletBackupDataSource ) { suspend operator fun invoke() { - walletBackupDataSource.remindMeLater() + walletBackupDataSource.onUserSavedWalletBackup() navigationRouter.backToRoot() } } \ No newline at end of file diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/RemindShieldFundsLaterUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/RemindShieldFundsLaterUseCase.kt new file mode 100644 index 000000000..ff95cf17f --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/RemindShieldFundsLaterUseCase.kt @@ -0,0 +1,15 @@ +package co.electriccoin.zcash.ui.common.usecase + +import co.electriccoin.zcash.ui.NavigationRouter +import co.electriccoin.zcash.ui.common.datasource.WalletBackupDataSource +import co.electriccoin.zcash.ui.common.repository.ShieldFundsRepository + +class RemindShieldFundsLaterUseCase( + private val navigationRouter: NavigationRouter, + private val shieldFundsRepository: ShieldFundsRepository +) { + suspend operator fun invoke() { + shieldFundsRepository.remindMeLater() + navigationRouter.backToRoot() + } +} \ No newline at end of file diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/RemindWalletBackupLaterUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/RemindWalletBackupLaterUseCase.kt index 724e5d616..d7ed38170 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/RemindWalletBackupLaterUseCase.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/RemindWalletBackupLaterUseCase.kt @@ -2,12 +2,17 @@ package co.electriccoin.zcash.ui.common.usecase import co.electriccoin.zcash.ui.NavigationRouter import co.electriccoin.zcash.ui.common.datasource.WalletBackupDataSource +import co.electriccoin.zcash.ui.common.provider.WalletBackupConsentStorageProvider class RemindWalletBackupLaterUseCase( private val navigationRouter: NavigationRouter, - private val walletBackupDataSource: WalletBackupDataSource + private val walletBackupDataSource: WalletBackupDataSource, + private val walletBackupConsentStorageProvider: WalletBackupConsentStorageProvider ) { - suspend operator fun invoke() { + suspend operator fun invoke(persistConsent: Boolean) { + if (persistConsent) { + walletBackupConsentStorageProvider.store(true) + } walletBackupDataSource.remindMeLater() navigationRouter.backToRoot() } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/ShieldFundsUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/ShieldFundsUseCase.kt new file mode 100644 index 000000000..8dc0fd1d7 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/ShieldFundsUseCase.kt @@ -0,0 +1,101 @@ +package co.electriccoin.zcash.ui.common.usecase + +import co.electriccoin.zcash.ui.NavigationRouter +import co.electriccoin.zcash.ui.common.datasource.AccountDataSource +import co.electriccoin.zcash.ui.common.model.KeystoneAccount +import co.electriccoin.zcash.ui.common.model.SubmitResult +import co.electriccoin.zcash.ui.common.model.ZashiAccount +import co.electriccoin.zcash.ui.common.repository.KeystoneProposalRepository +import co.electriccoin.zcash.ui.common.repository.SubmitProposalState +import co.electriccoin.zcash.ui.common.repository.ZashiProposalRepository +import co.electriccoin.zcash.ui.screen.signkeystonetransaction.SignKeystoneTransaction +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +class ShieldFundsUseCase( + private val keystoneProposalRepository: KeystoneProposalRepository, + private val zashiProposalRepository: ZashiProposalRepository, + private val navigationRouter: NavigationRouter, + private val accountDataSource: AccountDataSource +) { + private val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) + + operator fun invoke(navigateBackAfterSuccess: Boolean) { + scope.launch { + when (accountDataSource.getSelectedAccount()) { + is KeystoneAccount -> { + createKeystoneShieldProposal() + } + is ZashiAccount -> { + if (navigateBackAfterSuccess) { + navigationRouter.back() + } + shieldZashiFunds() + } + } + } + } + + private suspend fun shieldZashiFunds() { + try { + zashiProposalRepository.createShieldProposal() + zashiProposalRepository.submitTransaction() + val result = zashiProposalRepository.submitState + .filterIsInstance() + .first() + .submitResult + + when (result) { + is SubmitResult.Success -> { + // do nothing + // TODO messages + } + + is SubmitResult.SimpleTrxFailure.SimpleTrxFailureGrpc -> { + // showShieldingError(ShieldState.FailedGrpc) + // TODO messages + } + + is SubmitResult.SimpleTrxFailure -> { + // showShieldingError( + // ShieldState.Failed( + // error = result.toErrorMessage(), + // stackTrace = result.toErrorStacktrace() + // ) + // ) + // TODO messages + } + + is SubmitResult.MultipleTrxFailure -> { + // do nothing + } + } + } finally { + zashiProposalRepository.clear() + } + } + + private suspend fun createKeystoneShieldProposal() { + try { + keystoneProposalRepository.createShieldProposal() + keystoneProposalRepository.createPCZTFromProposal() + navigationRouter.forward(SignKeystoneTransaction) + } catch (e: Exception) { + keystoneProposalRepository.clear() + // TODO messages + // showShieldingError( + // ShieldState.Failed( + // error = + // context.getString( + // R.string.balances_shielding_dialog_error_text_below_threshold + // ), + // stackTrace = "" + // ) + // ) + } + } +} \ No newline at end of file diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/HomeMessage.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/HomeMessage.kt index 93e25aa95..c01c199bf 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/HomeMessage.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/HomeMessage.kt @@ -47,8 +47,8 @@ import co.electriccoin.zcash.ui.screen.home.restoring.WalletRestoringMessage import co.electriccoin.zcash.ui.screen.home.restoring.WalletRestoringMessageState import co.electriccoin.zcash.ui.screen.home.syncing.WalletSyncingMessage import co.electriccoin.zcash.ui.screen.home.syncing.WalletSyncingMessageState -import co.electriccoin.zcash.ui.screen.home.transparentbalance.TransparentBalanceMessage -import co.electriccoin.zcash.ui.screen.home.transparentbalance.TransparentBalanceMessageState +import co.electriccoin.zcash.ui.screen.home.shieldfunds.ShieldFundsMessage +import co.electriccoin.zcash.ui.screen.home.shieldfunds.ShieldFundsMessageState import co.electriccoin.zcash.ui.screen.home.updating.WalletUpdatingMessage import co.electriccoin.zcash.ui.screen.home.updating.WalletUpdatingMessageState import kotlinx.coroutines.delay @@ -69,10 +69,11 @@ fun HomeMessage( ) val bottomHeight by animateDpAsState( targetValue = if (isVisible) BOTTOM_CUTOUT_HEIGHT_DP.dp else TOP_CUTOUT_HEIGHT_DP.dp, - animationSpec = - animationSpec( - delay = if (isVisible) null else 250.milliseconds - ) + animationSpec = animationSpec(delay = if (isVisible) null else 250.milliseconds) + ) + val elevation by animateDpAsState( + targetValue = if (isVisible) 2.dp else 0.dp, + animationSpec = elevationAnimationSpec(delay = if (isVisible) 450.milliseconds else null) ) Box( @@ -85,7 +86,7 @@ fun HomeMessage( .height(TOP_CUTOUT_HEIGHT_DP.dp) .zIndex(2f) .bottomOnlyShadow( - elevation = 2.dp, + elevation = elevation, shape = RoundedCornerShape( bottomStart = TOP_CUTOUT_HEIGHT_DP.dp, @@ -128,10 +129,10 @@ fun HomeMessage( contentPadding = contentPadding ) - is TransparentBalanceMessageState -> - TransparentBalanceMessage( + is ShieldFundsMessageState -> + ShieldFundsMessage( innerModifier = innerModifier, - state = normalizedState as TransparentBalanceMessageState, + state = normalizedState as ShieldFundsMessageState, contentPadding = contentPadding ) @@ -184,7 +185,7 @@ fun HomeMessage( .zIndex(1f) .align(Alignment.BottomCenter) .topOnlyShadow( - elevation = 2.dp, + elevation = elevation, shape = RoundedCornerShape(topStart = bottomCornerSize, topEnd = bottomCornerSize), backgroundColor = ZashiColors.Surfaces.bgPrimary ), @@ -232,7 +233,17 @@ private fun animationSpec(delay: Duration? = null): TweenSpec { ) } +private fun elevationAnimationSpec(delay: Duration? = null): TweenSpec { + val delayMs = delay?.inWholeMilliseconds?.toInt() ?: 0 + return tween( + durationMillis = ELEVATION_ANIMATION_DURATION_MS - delayMs, + easing = CubicBezierEasing(0.6f, 0.1f, 0.3f, 0.9f), + delayMillis = delayMs + ) +} + private const val ANIMATION_DURATION_MS = 850 +private const val ELEVATION_ANIMATION_DURATION_MS = 650 private const val ANIMATION_DURATION_BETWEEN_MESSAGES_MS = 1000 private const val TOP_CUTOUT_HEIGHT_DP = 32 private const val BOTTOM_CUTOUT_HEIGHT_DP = 24 diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/HomeViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/HomeViewModel.kt index 22d9fc67c..05c8de263 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/HomeViewModel.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/HomeViewModel.kt @@ -15,29 +15,30 @@ import co.electriccoin.zcash.ui.common.usecase.GetSelectedWalletAccountUseCase import co.electriccoin.zcash.ui.common.usecase.HomeMessageData import co.electriccoin.zcash.ui.common.usecase.IsRestoreSuccessDialogVisibleUseCase import co.electriccoin.zcash.ui.common.usecase.NavigateToCoinbaseUseCase +import co.electriccoin.zcash.ui.common.usecase.ShieldFundsUseCase import co.electriccoin.zcash.ui.design.component.BigIconButtonState import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.screen.exchangerate.optin.ExchangeRateOptIn import co.electriccoin.zcash.ui.screen.home.backup.SeedBackupInfo -import co.electriccoin.zcash.ui.screen.home.currency.EnableCurrencyConversionMessageState -import co.electriccoin.zcash.ui.screen.home.transparentbalance.TransparentBalanceMessageState +import co.electriccoin.zcash.ui.screen.home.backup.WalletBackupDetail import co.electriccoin.zcash.ui.screen.home.backup.WalletBackupMessageState +import co.electriccoin.zcash.ui.screen.home.currency.EnableCurrencyConversionMessageState import co.electriccoin.zcash.ui.screen.home.disconnected.WalletDisconnectedInfo import co.electriccoin.zcash.ui.screen.home.disconnected.WalletDisconnectedMessageState import co.electriccoin.zcash.ui.screen.home.error.WalletErrorMessageState -import co.electriccoin.zcash.ui.screen.home.restoring.WalletRestoringMessageState -import co.electriccoin.zcash.ui.screen.home.syncing.WalletSyncingMessageState -import co.electriccoin.zcash.ui.screen.home.updating.WalletUpdatingMessageState import co.electriccoin.zcash.ui.screen.home.restoring.WalletRestoringInfo +import co.electriccoin.zcash.ui.screen.home.restoring.WalletRestoringMessageState +import co.electriccoin.zcash.ui.screen.home.shieldfunds.ShieldFundsInfo +import co.electriccoin.zcash.ui.screen.home.shieldfunds.ShieldFundsMessageState import co.electriccoin.zcash.ui.screen.home.syncing.WalletSyncingInfo -import co.electriccoin.zcash.ui.screen.home.transparentbalance.TransparentBalanceInfo +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.WalletUpdatingMessageState 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.scan.Scan import co.electriccoin.zcash.ui.screen.scan.ScanFlow -import co.electriccoin.zcash.ui.screen.home.backup.WalletBackupDetail import co.electriccoin.zcash.ui.screen.send.Send import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted @@ -55,6 +56,7 @@ class HomeViewModel( private val navigationRouter: NavigationRouter, private val isRestoreSuccessDialogVisible: IsRestoreSuccessDialogVisibleUseCase, private val navigateToCoinbase: NavigateToCoinbaseUseCase, + private val shieldFunds: ShieldFundsUseCase ) : ViewModel() { private val messageState = getHomeMessage @@ -177,10 +179,13 @@ class HomeViewModel( onClick = ::onWalletSyncingMessageClick ) - is HomeMessageData.TransparentBalance -> TransparentBalanceMessageState( - subtitle = stringRes(zatoshi = it.zatoshi), - onClick = ::onTransparentBalanceMessageClick, - onButtonClick = ::onTransparentBalanceMessageButtonClick, + is HomeMessageData.ShieldFunds -> ShieldFundsMessageState( + subtitle = stringRes( + R.string.home_message_transparent_balance_subtitle, + stringRes(it.zatoshi) + ), + onClick = ::onShieldFundsMessageClick, + onButtonClick = ::onShieldFundsMessageButtonClick, ) HomeMessageData.Updating -> WalletUpdatingMessageState( @@ -190,73 +195,40 @@ class HomeViewModel( null -> null } - private fun onRestoreDialogSeenClick() = - viewModelScope.launch { - isRestoreSuccessDialogVisible.setSeen() - } + private fun onRestoreDialogSeenClick() = viewModelScope.launch { isRestoreSuccessDialogVisible.setSeen() } - private fun onMoreButtonClick() { - navigationRouter.forward(DialogIntegrations) - } + private fun onMoreButtonClick() = navigationRouter.forward(DialogIntegrations) - private fun onSendButtonClick() { - navigationRouter.forward(Send()) - } + private fun onSendButtonClick() = navigationRouter.forward(Send()) - private fun onReceiveButtonClick() { - navigationRouter.forward(Receive) - } + private fun onReceiveButtonClick() = navigationRouter.forward(Receive) - 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() = navigationRouter.forward("${NavigationTargets.REQUEST}/${ReceiveAddressType.Unified.ordinal}") - } - private fun onWalletUpdatingMessageClick() { - navigationRouter.forward(WalletUpdatingInfo) - } + private fun onWalletUpdatingMessageClick() = navigationRouter.forward(WalletUpdatingInfo) - private fun onWalletSyncingMessageClick() { - navigationRouter.forward(WalletSyncingInfo) - } + private fun onWalletSyncingMessageClick() = navigationRouter.forward(WalletSyncingInfo) - private fun onWalletRestoringMessageClick() { - navigationRouter.forward(WalletRestoringInfo) - } + private fun onWalletRestoringMessageClick() = navigationRouter.forward(WalletRestoringInfo) - private fun onEnableCurrencyConversionClick() { - navigationRouter.forward(ExchangeRateOptIn) - } + private fun onEnableCurrencyConversionClick() = navigationRouter.forward(ExchangeRateOptIn) - private fun onWalletDisconnectedMessageClick() { - navigationRouter.forward(WalletDisconnectedInfo) - } + private fun onWalletDisconnectedMessageClick() = navigationRouter.forward(WalletDisconnectedInfo) - private fun onWalletBackupMessageClick() { - navigationRouter.forward(SeedBackupInfo) - } + private fun onWalletBackupMessageClick() = navigationRouter.forward(SeedBackupInfo) - private fun onWalletBackupMessageButtonClick() { - navigationRouter.forward(WalletBackupDetail(false)) - } + private fun onWalletBackupMessageButtonClick() = navigationRouter.forward(WalletBackupDetail(false)) - private fun onTransparentBalanceMessageClick() { - navigationRouter.forward(TransparentBalanceInfo) - } + private fun onShieldFundsMessageClick() = navigationRouter.forward(ShieldFundsInfo) - private fun onTransparentBalanceMessageButtonClick(): Nothing { - TODO() - } + private fun onShieldFundsMessageButtonClick() = shieldFunds(navigateBackAfterSuccess = false) - private fun onWalletErrorMessageClick(homeMessageData: HomeMessageData.Error): Nothing { + private fun onWalletErrorMessageClick(homeMessageData: HomeMessageData.Error) { // statusText = // context.getString( // R.string.balances_status_error_simple, diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/backup/WalletBackupInfoState.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/backup/WalletBackupInfoState.kt index 18e4859fb..91c5721cb 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/backup/WalletBackupInfoState.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/backup/WalletBackupInfoState.kt @@ -1,10 +1,12 @@ package co.electriccoin.zcash.ui.screen.home.backup import co.electriccoin.zcash.ui.design.component.ButtonState +import co.electriccoin.zcash.ui.design.component.CheckboxState import co.electriccoin.zcash.ui.design.component.ModalBottomSheetState data class WalletBackupInfoState( override val onBack: () -> Unit, + val checkboxState: CheckboxState?, val primaryButton: ButtonState, val secondaryButton: ButtonState ) : ModalBottomSheetState diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/backup/WalletBackupInfoView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/backup/WalletBackupInfoView.kt index d268d538a..630c47d5d 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/backup/WalletBackupInfoView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/backup/WalletBackupInfoView.kt @@ -4,6 +4,8 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SheetState import androidx.compose.material3.Text @@ -15,10 +17,12 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.design.component.ButtonState +import co.electriccoin.zcash.ui.design.component.CheckboxState import co.electriccoin.zcash.ui.design.component.Spacer import co.electriccoin.zcash.ui.design.component.ZashiBulletText import co.electriccoin.zcash.ui.design.component.ZashiButton import co.electriccoin.zcash.ui.design.component.ZashiButtonDefaults +import co.electriccoin.zcash.ui.design.component.ZashiCheckbox import co.electriccoin.zcash.ui.design.component.ZashiScreenModalBottomSheet import co.electriccoin.zcash.ui.design.component.rememberScreenModalBottomSheetState import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens @@ -38,9 +42,11 @@ fun WalletBackupInfoView( state = state ) { Column( - modifier = - Modifier - .padding(horizontal = 24.dp) + modifier = Modifier + .weight(1f, false) + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp) + ) { Image( painter = painterResource(R.drawable.ic_info_backup), @@ -84,6 +90,12 @@ fun WalletBackupInfoView( style = ZashiTypography.textMd ) Spacer(32.dp) + it.checkboxState?.let { checkbox -> + ZashiCheckbox( + state = checkbox + ) + Spacer(12.dp) + } ZashiButton( modifier = Modifier.fillMaxWidth(), state = it.secondaryButton, @@ -108,11 +120,17 @@ private fun Preview() = onBack = {}, secondaryButton = ButtonState( text = stringRes(R.string.general_remind_me_later), - onClick = {} + onClick = {}, + isEnabled = false ), primaryButton = ButtonState( text = stringRes(R.string.general_ok), onClick = {} + ), + checkboxState = CheckboxState( + isChecked = false, + onClick = {}, + text = stringRes(R.string.home_info_backup_checkbox) ) ) ) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/backup/WalletBackupInfoViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/backup/WalletBackupInfoViewModel.kt index 98d59652f..5367126e9 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/backup/WalletBackupInfoViewModel.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/backup/WalletBackupInfoViewModel.kt @@ -2,38 +2,83 @@ package co.electriccoin.zcash.ui.screen.home.backup import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT import co.electriccoin.zcash.ui.NavigationRouter import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.common.datasource.WalletBackupAvailability +import co.electriccoin.zcash.ui.common.datasource.WalletBackupDataSource +import co.electriccoin.zcash.ui.common.provider.WalletBackupConsentStorageProvider import co.electriccoin.zcash.ui.common.usecase.RemindWalletBackupLaterUseCase import co.electriccoin.zcash.ui.design.component.ButtonState +import co.electriccoin.zcash.ui.design.component.CheckboxState import co.electriccoin.zcash.ui.design.util.stringRes import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.WhileSubscribed +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch class WalletBackupInfoViewModel( + walletBackupDataSource: WalletBackupDataSource, + walletBackupConsentStorageProvider: WalletBackupConsentStorageProvider, private val navigationRouter: NavigationRouter, - private val remindWalletBackupLater: RemindWalletBackupLaterUseCase + private val remindWalletBackupLater: RemindWalletBackupLaterUseCase, ) : ViewModel() { - val state: StateFlow = MutableStateFlow( - WalletBackupInfoState( - onBack = ::onBack, - secondaryButton = ButtonState( - text = stringRes(R.string.general_remind_me_later), - onClick = ::onRemindMeLaterClick - ), - primaryButton = ButtonState( - text = stringRes(R.string.general_ok), - onClick = ::onPrimaryClick - ) + + private val isConsentChecked = MutableStateFlow(false) + + private val lockoutDuration = walletBackupDataSource + .observe() + .filterIsInstance() + .take(1) + .map { it.lockoutDuration } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null ) - ) - private fun onPrimaryClick() { - navigationRouter.replace(WalletBackupDetail(true)) - } + val state: StateFlow = combine( + lockoutDuration.filterNotNull(), + isConsentChecked, + walletBackupConsentStorageProvider.observe().take(1) + ) { lockout, isConsentChecked, isConsentSaved -> + WalletBackupInfoState( + onBack = ::onBack, + secondaryButton = ButtonState( + text = stringRes(R.string.general_remind_me_in, stringRes(lockout.res)), + onClick = ::onRemindMeLaterClick, + isEnabled = isConsentChecked || isConsentSaved + ), + checkboxState = CheckboxState( + isChecked = isConsentChecked, + onClick = ::onConsentClick, + text = stringRes(R.string.home_info_backup_checkbox) + ).takeIf { !isConsentSaved }, + primaryButton = ButtonState( + text = stringRes(R.string.general_ok), + onClick = ::onPrimaryClick + ) + ) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), + initialValue = null + ) - private fun onRemindMeLaterClick() = viewModelScope.launch { remindWalletBackupLater() } + private fun onConsentClick() = isConsentChecked.update { !it } + + private fun onPrimaryClick() = navigationRouter.replace(WalletBackupDetail(true)) + + private fun onRemindMeLaterClick() = viewModelScope.launch { remindWalletBackupLater(persistConsent = true) } private fun onBack() = navigationRouter.back() } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/transparentbalance/AndroidTransparentBalanceInfo.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/shieldfunds/AndroidShieldFundsInfo.kt similarity index 63% rename from ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/transparentbalance/AndroidTransparentBalanceInfo.kt rename to ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/shieldfunds/AndroidShieldFundsInfo.kt index 6fc0711a6..2b45ecd4f 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/transparentbalance/AndroidTransparentBalanceInfo.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/shieldfunds/AndroidShieldFundsInfo.kt @@ -1,4 +1,4 @@ -package co.electriccoin.zcash.ui.screen.home.transparentbalance +package co.electriccoin.zcash.ui.screen.home.shieldfunds import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable @@ -9,11 +9,11 @@ import org.koin.androidx.compose.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AndroidTransparentBalanceInfo() { - val vm = koinViewModel() +fun AndroidShieldFundsInfo() { + val vm = koinViewModel() val state by vm.state.collectAsStateWithLifecycle() - state?.let { TransparentBalanceInfoView(it) } + ShieldFundsInfoView(state) } @Serializable -object TransparentBalanceInfo +object ShieldFundsInfo diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/transparentbalance/TransparentBalanceInfoState.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/shieldfunds/ShieldFundsInfoState.kt similarity index 77% rename from ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/transparentbalance/TransparentBalanceInfoState.kt rename to ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/shieldfunds/ShieldFundsInfoState.kt index b28573abb..19641da69 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/transparentbalance/TransparentBalanceInfoState.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/shieldfunds/ShieldFundsInfoState.kt @@ -1,10 +1,10 @@ -package co.electriccoin.zcash.ui.screen.home.transparentbalance +package co.electriccoin.zcash.ui.screen.home.shieldfunds import cash.z.ecc.android.sdk.model.Zatoshi import co.electriccoin.zcash.ui.design.component.ButtonState import co.electriccoin.zcash.ui.design.component.ModalBottomSheetState -data class TransparentBalanceInfoState( +data class ShieldFundsInfoState( val transparentAmount: Zatoshi, override val onBack: () -> Unit, val primaryButton: ButtonState, diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/transparentbalance/TransparentBalanceInfoView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/shieldfunds/ShieldFundsInfoView.kt similarity index 94% rename from ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/transparentbalance/TransparentBalanceInfoView.kt rename to ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/shieldfunds/ShieldFundsInfoView.kt index 2414cfdca..99ccd66f3 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/transparentbalance/TransparentBalanceInfoView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/shieldfunds/ShieldFundsInfoView.kt @@ -1,4 +1,4 @@ -package co.electriccoin.zcash.ui.screen.home.transparentbalance +package co.electriccoin.zcash.ui.screen.home.shieldfunds import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column @@ -34,8 +34,8 @@ import co.electriccoin.zcash.ui.design.util.stringRes @OptIn(ExperimentalMaterial3Api::class) @Composable -fun TransparentBalanceInfoView( - state: TransparentBalanceInfoState?, +fun ShieldFundsInfoView( + state: ShieldFundsInfoState?, sheetState: SheetState = rememberScreenModalBottomSheetState(), ) { ZashiScreenModalBottomSheet( @@ -47,7 +47,7 @@ fun TransparentBalanceInfoView( } @Composable -private fun Content(state: TransparentBalanceInfoState) { +private fun Content(state: ShieldFundsInfoState) { Column( modifier = Modifier @@ -132,9 +132,9 @@ private fun Content(state: TransparentBalanceInfoState) { @Composable private fun Preview() = ZcashTheme { - TransparentBalanceInfoView( + ShieldFundsInfoView( state = - TransparentBalanceInfoState( + ShieldFundsInfoState( onBack = {}, primaryButton = ButtonState( diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/shieldfunds/ShieldFundsInfoViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/shieldfunds/ShieldFundsInfoViewModel.kt new file mode 100644 index 000000000..50c6e2467 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/shieldfunds/ShieldFundsInfoViewModel.kt @@ -0,0 +1,79 @@ +package co.electriccoin.zcash.ui.screen.home.shieldfunds + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import cash.z.ecc.android.sdk.model.Zatoshi +import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT +import co.electriccoin.zcash.ui.NavigationRouter +import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.common.datasource.WalletBackupAvailability +import co.electriccoin.zcash.ui.common.repository.ShieldFundsData +import co.electriccoin.zcash.ui.common.repository.ShieldFundsRepository +import co.electriccoin.zcash.ui.common.usecase.GetSelectedWalletAccountUseCase +import co.electriccoin.zcash.ui.common.usecase.RemindShieldFundsLaterUseCase +import co.electriccoin.zcash.ui.common.usecase.ShieldFundsUseCase +import co.electriccoin.zcash.ui.design.component.ButtonState +import co.electriccoin.zcash.ui.design.util.stringRes +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.WhileSubscribed +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch + +class ShieldFundsInfoViewModel( + getSelectedWalletAccount: GetSelectedWalletAccountUseCase, + shieldFundsRepository: ShieldFundsRepository, + private val navigationRouter: NavigationRouter, + private val remindShieldFundsLater: RemindShieldFundsLaterUseCase, + private val shieldFunds: ShieldFundsUseCase, +) : ViewModel() { + + private val lockoutDuration = shieldFundsRepository + .availability + .filterIsInstance() + .take(1) + .map { it.lockoutDuration } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null + ) + + val state: StateFlow = + combine( + getSelectedWalletAccount.observe(), + lockoutDuration.filterNotNull(), + ) { account, lockoutDuration -> + ShieldFundsInfoState( + onBack = ::onBack, + primaryButton = + ButtonState( + onClick = ::onShieldClick, + text = stringRes(R.string.home_info_transparent_balance_shield) + ), + secondaryButton = + ButtonState( + onClick = ::onRemindMeClick, + text = stringRes(R.string.general_remind_me_in, stringRes(lockoutDuration.res)) + ), + transparentAmount = account?.transparent?.balance ?: Zatoshi(0) + ) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), + initialValue = null + ) + + private fun onRemindMeClick() = viewModelScope.launch { remindShieldFundsLater() } + + private fun onBack() = navigationRouter.back() + + private fun onShieldClick() = shieldFunds(navigateBackAfterSuccess = true) +} + diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/transparentbalance/TransparentBalanceMessage.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/shieldfunds/ShieldFundsMessage.kt similarity index 91% rename from ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/transparentbalance/TransparentBalanceMessage.kt rename to ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/shieldfunds/ShieldFundsMessage.kt index 3629c3918..83465c5ef 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/transparentbalance/TransparentBalanceMessage.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/shieldfunds/ShieldFundsMessage.kt @@ -1,4 +1,4 @@ -package co.electriccoin.zcash.ui.screen.home.transparentbalance +package co.electriccoin.zcash.ui.screen.home.shieldfunds import androidx.compose.foundation.Image import androidx.compose.foundation.layout.PaddingValues @@ -26,9 +26,9 @@ import co.electriccoin.zcash.ui.screen.home.HomeMessageWrapper @Suppress("ModifierNaming") @Composable -fun TransparentBalanceMessage( +fun ShieldFundsMessage( contentPadding: PaddingValues, - state: TransparentBalanceMessageState, + state: ShieldFundsMessageState, innerModifier: Modifier = Modifier, ) { HomeMessageWrapper( @@ -65,7 +65,7 @@ fun TransparentBalanceMessage( ) } -class TransparentBalanceMessageState( +class ShieldFundsMessageState( val subtitle: StringResource, val onClick: () -> Unit, val onButtonClick: () -> Unit, @@ -76,9 +76,9 @@ class TransparentBalanceMessageState( private fun Preview() = ZcashTheme { BlankSurface { - TransparentBalanceMessage( + ShieldFundsMessage( state = - TransparentBalanceMessageState( + ShieldFundsMessageState( subtitle = stringRes( R.string.home_message_transparent_balance_subtitle, diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/transparentbalance/TransparentBalanceInfoViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/transparentbalance/TransparentBalanceInfoViewModel.kt deleted file mode 100644 index f0312453f..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/transparentbalance/TransparentBalanceInfoViewModel.kt +++ /dev/null @@ -1,45 +0,0 @@ -package co.electriccoin.zcash.ui.screen.home.transparentbalance - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import cash.z.ecc.android.sdk.model.Zatoshi -import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT -import co.electriccoin.zcash.ui.NavigationRouter -import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.common.usecase.GetSelectedWalletAccountUseCase -import co.electriccoin.zcash.ui.design.component.ButtonState -import co.electriccoin.zcash.ui.design.util.stringRes -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.WhileSubscribed -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn - -class TransparentBalanceInfoViewModel( - getSelectedWalletAccount: GetSelectedWalletAccountUseCase, - private val navigationRouter: NavigationRouter -) : ViewModel() { - val state: StateFlow = - getSelectedWalletAccount - .observe() - .map { - TransparentBalanceInfoState( - onBack = { navigationRouter.back() }, - primaryButton = - ButtonState( - onClick = { navigationRouter.back() }, - text = stringRes(R.string.home_info_transparent_balance_shield) - ), - secondaryButton = - ButtonState( - onClick = { navigationRouter.back() }, - text = stringRes(R.string.general_remind_me_later) - ), - transparentAmount = it?.transparent?.balance ?: Zatoshi(0) - ) - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), - initialValue = null - ) -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/viewmodel/ReceiveViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/viewmodel/ReceiveViewModel.kt index 17475f815..25954345d 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/viewmodel/ReceiveViewModel.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/viewmodel/ReceiveViewModel.kt @@ -11,7 +11,6 @@ import co.electriccoin.zcash.ui.common.model.KeystoneAccount import co.electriccoin.zcash.ui.common.model.WalletAccount import co.electriccoin.zcash.ui.common.model.ZashiAccount import co.electriccoin.zcash.ui.common.usecase.CopyToClipboardUseCase -import co.electriccoin.zcash.ui.common.usecase.ObserveOnAccountChangedUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedWalletAccountUseCase import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.ADDRESS_MAX_LENGTH @@ -24,11 +23,9 @@ import kotlinx.coroutines.flow.WhileSubscribed import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch class ReceiveViewModel( observeSelectedWalletAccount: ObserveSelectedWalletAccountUseCase, - observeOnAccountChanged: ObserveOnAccountChangedUseCase, private val application: Application, private val copyToClipboard: CopyToClipboardUseCase, private val navigationRouter: NavigationRouter, @@ -69,14 +66,6 @@ class ReceiveViewModel( ) ) - init { - viewModelScope.launch { - observeOnAccountChanged().collect { - expandedIndex.update { 0 } - } - } - } - private fun onBack() = navigationRouter.back() private fun createAddressState( diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/sendconfirmation/viewmodel/CreateTransactionsViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/sendconfirmation/viewmodel/CreateTransactionsViewModel.kt deleted file mode 100644 index cfe56264e..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/sendconfirmation/viewmodel/CreateTransactionsViewModel.kt +++ /dev/null @@ -1,65 +0,0 @@ -package co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel - -import androidx.lifecycle.ViewModel -import cash.z.ecc.android.sdk.Synchronizer -import cash.z.ecc.android.sdk.model.Proposal -import cash.z.ecc.android.sdk.model.TransactionSubmitResult -import cash.z.ecc.android.sdk.model.UnifiedSpendingKey -import co.electriccoin.zcash.spackle.Twig -import co.electriccoin.zcash.ui.common.model.SubmitResult -import kotlinx.coroutines.flow.MutableStateFlow - -class CreateTransactionsViewModel : ViewModel() { - // Technically this value will not survive process dead, but will survive all possible configuration changes - // Possible solution would be storing the value within [SavedStateHandle] - val submissions: MutableStateFlow> = MutableStateFlow(emptyList()) - - suspend fun runCreateTransactions( - synchronizer: Synchronizer, - spendingKey: UnifiedSpendingKey, - proposal: Proposal - ): SubmitResult { - val submitResults = mutableListOf() - - return runCatching { - synchronizer - .createProposedTransactions( - proposal = proposal, - usk = spendingKey - ).collect { submitResult -> - Twig.info { "Transaction submit result: $submitResult" } - submitResults.add(submitResult) - } - if (submitResults.find { it is TransactionSubmitResult.Failure } != null) { - if (submitResults.size == 1) { - // The first transaction submission failed - user might just be able to re-submit the transaction - // proposal. Simple error pop up is fine then - val result = (submitResults[0] as TransactionSubmitResult.Failure) - if (result.grpcError) { - SubmitResult.SimpleTrxFailure.SimpleTrxFailureGrpc(result) - } else { - SubmitResult.SimpleTrxFailure.SimpleTrxFailureSubmit(result) - } - } else { - // Any subsequent transaction submission failed - user needs to resolve this manually. Multiple - // transaction failure screen presented - SubmitResult.MultipleTrxFailure(submitResults) - } - } else { - // All transaction submissions were successful - SubmitResult.Success(emptyList()) - } - }.onSuccess { - Twig.debug { "Transactions submitted successfully" } - }.onFailure { - Twig.error(it) { "Transactions submission failed" } - }.getOrElse { - SubmitResult.SimpleTrxFailure.SimpleTrxFailureOther(it) - }.also { - // Save the submission results for the later MultipleSubmissionError screen - if (it is SubmitResult.MultipleTrxFailure) { - submissions.value = submitResults - } - } - } -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/transactionfilters/AndroidTransactionFilters.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/transactionfilters/AndroidTransactionFilters.kt index 571e4a52e..3acc064af 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/transactionfilters/AndroidTransactionFilters.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/transactionfilters/AndroidTransactionFilters.kt @@ -13,7 +13,5 @@ import org.koin.androidx.compose.koinViewModel fun AndroidTransactionFiltersList() { val viewModel = koinViewModel() val state by viewModel.state.collectAsStateWithLifecycle() - TransactionFiltersView( - state = state, - ) + TransactionFiltersView(state = state) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/walletbackup/WalletBackupViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/walletbackup/WalletBackupViewModel.kt index f46291d8f..a276670b6 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/walletbackup/WalletBackupViewModel.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/walletbackup/WalletBackupViewModel.kt @@ -5,6 +5,8 @@ import androidx.lifecycle.viewModelScope import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT import co.electriccoin.zcash.ui.NavigationRouter import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.common.datasource.WalletBackupAvailability +import co.electriccoin.zcash.ui.common.datasource.WalletBackupDataSource import co.electriccoin.zcash.ui.common.usecase.ObservePersistableWalletUseCase import co.electriccoin.zcash.ui.common.usecase.OnUserSavedWalletBackupUseCase import co.electriccoin.zcash.ui.common.usecase.RemindWalletBackupLaterUseCase @@ -17,18 +19,33 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.WhileSubscribed import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch class WalletBackupViewModel( - private val args: WalletBackup, + walletBackupDataSource: WalletBackupDataSource, observePersistableWallet: ObservePersistableWalletUseCase, + private val args: WalletBackup, private val navigationRouter: NavigationRouter, private val onUserSavedWalletBackup: OnUserSavedWalletBackupUseCase, - private val remindWalletBackupLater: RemindWalletBackupLaterUseCase + private val remindWalletBackupLater: RemindWalletBackupLaterUseCase, ) : ViewModel() { + + private val lockoutDuration = walletBackupDataSource + .observe() + .filterIsInstance() + .take(1) + .map { it.lockoutDuration } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null + ) + private val isRevealed = MutableStateFlow(false) private val isRemindMeLaterButtonVisible = isRevealed @@ -42,11 +59,16 @@ class WalletBackupViewModel( combine( isRevealed, isRemindMeLaterButtonVisible, - observableWallet - ) { isRevealed, isRemindMeLaterButtonVisible, wallet -> + observableWallet, + lockoutDuration + ) { isRevealed, isRemindMeLaterButtonVisible, wallet, lockoutDuration -> WalletBackupState( secondaryButton = ButtonState( - text = stringRes(R.string.general_remind_me_later), + text = if (lockoutDuration != null) { + stringRes(R.string.general_remind_me_in, stringRes(lockoutDuration.res)) + } else { + stringRes(R.string.general_remind_me_later) + }, onClick = ::onRemindMeLaterClick ).takeIf { isRemindMeLaterButtonVisible }, primaryButton = @@ -105,10 +127,7 @@ class WalletBackupViewModel( ) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), null) - private fun onRemindMeLaterClick() = - viewModelScope.launch { - remindWalletBackupLater() - } + private fun onRemindMeLaterClick() = viewModelScope.launch { remindWalletBackupLater(persistConsent = false) } private fun onWalletBackupSavedClick() = viewModelScope.launch { diff --git a/ui-lib/src/main/res/ui/common/values/strings.xml b/ui-lib/src/main/res/ui/common/values/strings.xml index dbdd725c5..53ca5ebfb 100644 --- a/ui-lib/src/main/res/ui/common/values/strings.xml +++ b/ui-lib/src/main/res/ui/common/values/strings.xml @@ -30,4 +30,8 @@ OK Remind me later + Remind me in %s + two days + two weeks + two months diff --git a/ui-lib/src/main/res/ui/home/values-es/strings.xml b/ui-lib/src/main/res/ui/home/values-es/strings.xml index 4e543b98e..d986b3855 100644 --- a/ui-lib/src/main/res/ui/home/values-es/strings.xml +++ b/ui-lib/src/main/res/ui/home/values-es/strings.xml @@ -36,6 +36,7 @@ Recovery Phrase is the only way to recover your funds. We cannot see it and cannot help you recover it. Anyone with access to your Secret Recovery Phrase will have full control of your wallet, so keep it secure and never show it to anyone. + I read and understand the risks of not backing up my wallet. Shield Always Shield Transparent Funds To protect user privacy, Zashi doesn\'t support spending transparent diff --git a/ui-lib/src/main/res/ui/home/values/strings.xml b/ui-lib/src/main/res/ui/home/values/strings.xml index 4e543b98e..d986b3855 100644 --- a/ui-lib/src/main/res/ui/home/values/strings.xml +++ b/ui-lib/src/main/res/ui/home/values/strings.xml @@ -36,6 +36,7 @@ Recovery Phrase is the only way to recover your funds. We cannot see it and cannot help you recover it. Anyone with access to your Secret Recovery Phrase will have full control of your wallet, so keep it secure and never show it to anyone. + I read and understand the risks of not backing up my wallet. Shield Always Shield Transparent Funds To protect user privacy, Zashi doesn\'t support spending transparent