* [#1568] Integrations screen Closes #1568 * [#1568] Icon added to integrations list item Closes #1568 * [#1568] Test hotfix Closes #1568 * [#1568] Code cleanup Closes #1568 * [#1568] Code cleanup Closes #1568
This commit is contained in:
parent
0e67d826d3
commit
d6f630cab8
|
@ -16,6 +16,7 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
|
|||
|
||||
### Added
|
||||
- Address book local and remote storage support
|
||||
- New Integrations screen in settings
|
||||
- New QR Code detail screen has been added
|
||||
- The new Request ZEC screens have been added. They provide a way to build ZIP 321 Uri consisting of the amount,
|
||||
message, and receiver address and then creates a QR code image of it.
|
||||
|
|
|
@ -18,6 +18,7 @@ directly impact users rather than highlighting other key architectural updates.*
|
|||
- Address Book, Create/Update/Delete Contact, Create Contact by QR screens added
|
||||
|
||||
### Added
|
||||
- New Integrations screen in settings
|
||||
- Address book local and remote storage support
|
||||
- New QR Code detail screen has been added
|
||||
- The new Request ZEC screens have been added. They provide a way to build ZIP 321 Uri consisting of the amount, message, and receiver address and then creates a QR code image of it.
|
||||
|
|
|
@ -34,11 +34,14 @@ import co.electriccoin.zcash.ui.design.util.StringResource
|
|||
import co.electriccoin.zcash.ui.design.util.getValue
|
||||
import co.electriccoin.zcash.ui.design.util.orDark
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
fun ZashiSettingsListItem(
|
||||
text: String,
|
||||
@DrawableRes icon: Int,
|
||||
titleIcons: ImmutableList<Int> = persistentListOf(),
|
||||
subtitle: String? = null,
|
||||
isEnabled: Boolean = true,
|
||||
onClick: () -> Unit
|
||||
|
@ -49,22 +52,20 @@ fun ZashiSettingsListItem(
|
|||
text = stringRes(text),
|
||||
subtitle = subtitle?.let { stringRes(it) },
|
||||
isEnabled = isEnabled,
|
||||
onClick = onClick
|
||||
onClick = onClick,
|
||||
icon = icon,
|
||||
titleIcons = titleIcons
|
||||
),
|
||||
icon = icon,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZashiSettingsListItem(
|
||||
state: ZashiSettingsListItemState,
|
||||
@DrawableRes icon: Int
|
||||
) {
|
||||
fun ZashiSettingsListItem(state: ZashiSettingsListItemState) {
|
||||
ZashiSettingsListItem(
|
||||
leading = { modifier ->
|
||||
ZashiSettingsListLeadingItem(
|
||||
modifier = modifier,
|
||||
icon = icon,
|
||||
icon = state.icon,
|
||||
contentDescription = state.text.getValue()
|
||||
)
|
||||
},
|
||||
|
@ -72,7 +73,8 @@ fun ZashiSettingsListItem(
|
|||
ZashiSettingsListContentItem(
|
||||
modifier = modifier,
|
||||
text = state.text.getValue(),
|
||||
subtitle = state.subtitle?.getValue()
|
||||
subtitle = state.subtitle?.getValue(),
|
||||
titleIcons = state.titleIcons
|
||||
)
|
||||
},
|
||||
trailing = { modifier ->
|
||||
|
@ -127,17 +129,28 @@ fun ZashiSettingsListTrailingItem(
|
|||
fun ZashiSettingsListContentItem(
|
||||
text: String,
|
||||
subtitle: String?,
|
||||
titleIcons: ImmutableList<Int>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = ZashiTypography.textMd,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = ZashiColors.Text.textPrimary
|
||||
)
|
||||
Row {
|
||||
Text(
|
||||
text = text,
|
||||
style = ZashiTypography.textMd,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = ZashiColors.Text.textPrimary
|
||||
)
|
||||
titleIcons.forEach {
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Image(
|
||||
modifier = Modifier.size(20.dp),
|
||||
painter = painterResource(it),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
subtitle?.let {
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
|
@ -185,12 +198,13 @@ fun ZashiSettingsListItem(
|
|||
|
||||
data class ZashiSettingsListItemState(
|
||||
val text: StringResource,
|
||||
@DrawableRes val icon: Int,
|
||||
val subtitle: StringResource? = null,
|
||||
val titleIcons: ImmutableList<Int> = persistentListOf(),
|
||||
val isEnabled: Boolean = true,
|
||||
val onClick: () -> Unit = {},
|
||||
)
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun EnabledPreview() =
|
||||
|
@ -200,12 +214,12 @@ private fun EnabledPreview() =
|
|||
text = "Test",
|
||||
subtitle = "Subtitle",
|
||||
icon = R.drawable.ic_radio_button_checked,
|
||||
onClick = {}
|
||||
onClick = {},
|
||||
titleIcons = persistentListOf(R.drawable.ic_radio_button_checked)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun DisabledPreview() =
|
||||
|
@ -216,7 +230,7 @@ private fun DisabledPreview() =
|
|||
subtitle = "Subtitle",
|
||||
icon = R.drawable.ic_radio_button_checked,
|
||||
isEnabled = false,
|
||||
onClick = {}
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ android {
|
|||
"src/main/res/ui/export_data",
|
||||
"src/main/res/ui/home",
|
||||
"src/main/res/ui/choose_server",
|
||||
"src/main/res/ui/integrations",
|
||||
"src/main/res/ui/new_wallet_recovery",
|
||||
"src/main/res/ui/onboarding",
|
||||
"src/main/res/ui/qr_code",
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package co.electriccoin.zcash.ui.screen.settings
|
||||
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItemState
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.settings.model.SettingsState
|
||||
|
@ -120,7 +122,12 @@ class SettingsViewTestSetup(
|
|||
},
|
||||
onAddressBookClick = {
|
||||
onAddressBookCount.incrementAndGet()
|
||||
}
|
||||
},
|
||||
integrations =
|
||||
ZashiSettingsListItemState(
|
||||
stringRes("Integrations"),
|
||||
R.drawable.ic_settings_integrations,
|
||||
)
|
||||
),
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
)
|
||||
|
|
|
@ -2,8 +2,12 @@ package co.electriccoin.zcash.di
|
|||
|
||||
import co.electriccoin.zcash.ui.common.repository.AddressBookRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.AddressBookRepositoryImpl
|
||||
import co.electriccoin.zcash.ui.common.repository.BalanceRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.BalanceRepositoryImpl
|
||||
import co.electriccoin.zcash.ui.common.repository.ConfigurationRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.ConfigurationRepositoryImpl
|
||||
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepositoryImpl
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletRepositoryImpl
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
|
@ -14,5 +18,7 @@ val repositoryModule =
|
|||
module {
|
||||
singleOf(::WalletRepositoryImpl) bind WalletRepository::class
|
||||
singleOf(::ConfigurationRepositoryImpl) bind ConfigurationRepository::class
|
||||
singleOf(::ExchangeRateRepositoryImpl) bind ExchangeRateRepository::class
|
||||
singleOf(::BalanceRepositoryImpl) bind BalanceRepository::class
|
||||
singleOf(::AddressBookRepositoryImpl) bind AddressBookRepository::class
|
||||
}
|
||||
|
|
|
@ -6,8 +6,10 @@ import co.electriccoin.zcash.ui.common.usecase.GetAddressesUseCase
|
|||
import co.electriccoin.zcash.ui.common.usecase.GetContactByAddressUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetPersistableWalletUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetSelectedEndpointUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetSpendingKeyUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetTransparentAddressUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.IsCoinbaseAvailableUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveAddressBookContactsUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveContactByAddressUseCase
|
||||
|
@ -15,6 +17,7 @@ import co.electriccoin.zcash.ui.common.usecase.ObserveContactPickedUseCase
|
|||
import co.electriccoin.zcash.ui.common.usecase.ObserveFastestServersUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedEndpointUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveSynchronizerUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveWalletStateUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.PersistEndpointUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.RefreshFastestServersUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.RescanBlockchainUseCase
|
||||
|
@ -53,6 +56,9 @@ val useCaseModule =
|
|||
singleOf(::ObserveContactPickedUseCase)
|
||||
singleOf(::GetAddressesUseCase)
|
||||
singleOf(::CopyToClipboardUseCase)
|
||||
singleOf(::ObserveWalletStateUseCase)
|
||||
singleOf(::IsCoinbaseAvailableUseCase)
|
||||
singleOf(::GetSpendingKeyUseCase)
|
||||
singleOf(::ShareImageUseCase)
|
||||
singleOf(::Zip321BuildUriUseCase)
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import co.electriccoin.zcash.ui.screen.advancedsettings.viewmodel.AdvancedSettin
|
|||
import co.electriccoin.zcash.ui.screen.chooseserver.ChooseServerViewModel
|
||||
import co.electriccoin.zcash.ui.screen.contact.viewmodel.AddContactViewModel
|
||||
import co.electriccoin.zcash.ui.screen.contact.viewmodel.UpdateContactViewModel
|
||||
import co.electriccoin.zcash.ui.screen.integrations.viewmodel.IntegrationsViewModel
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
|
||||
import co.electriccoin.zcash.ui.screen.qrcode.viewmodel.QrCodeViewModel
|
||||
import co.electriccoin.zcash.ui.screen.receive.viewmodel.ReceiveViewModel
|
||||
|
@ -66,5 +67,6 @@ val viewModelModule =
|
|||
viewModelOf(::UpdateContactViewModel)
|
||||
viewModelOf(::ReceiveViewModel)
|
||||
viewModelOf(::QrCodeViewModel)
|
||||
viewModelOf(::IntegrationsViewModel)
|
||||
viewModelOf(::RequestViewModel)
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import co.electriccoin.zcash.ui.NavigationTargets.DELETE_WALLET
|
|||
import co.electriccoin.zcash.ui.NavigationTargets.EXCHANGE_RATE_OPT_IN
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.EXPORT_PRIVATE_DATA
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.HOME
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.INTEGRATIONS
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.NOT_ENOUGH_SPACE
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.QR_CODE
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.REQUEST
|
||||
|
@ -69,6 +70,7 @@ import co.electriccoin.zcash.ui.screen.exchangerate.optin.AndroidExchangeRateOpt
|
|||
import co.electriccoin.zcash.ui.screen.exchangerate.settings.AndroidSettingsExchangeRateOptIn
|
||||
import co.electriccoin.zcash.ui.screen.exportdata.WrapExportPrivateData
|
||||
import co.electriccoin.zcash.ui.screen.home.WrapHome
|
||||
import co.electriccoin.zcash.ui.screen.integrations.WrapIntegrations
|
||||
import co.electriccoin.zcash.ui.screen.qrcode.WrapQrCode
|
||||
import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType
|
||||
import co.electriccoin.zcash.ui.screen.request.WrapRequest
|
||||
|
@ -225,6 +227,9 @@ internal fun MainActivity.Navigation() {
|
|||
composable(WHATS_NEW) {
|
||||
WrapWhatsNew()
|
||||
}
|
||||
composable(INTEGRATIONS) {
|
||||
WrapIntegrations()
|
||||
}
|
||||
composable(EXCHANGE_RATE_OPT_IN) {
|
||||
AndroidExchangeRateOptIn()
|
||||
}
|
||||
|
@ -539,6 +544,7 @@ object NavigationTargets {
|
|||
const val SETTINGS_EXCHANGE_RATE_OPT_IN = "settings_exchange_rate_opt_in"
|
||||
const val SUPPORT = "support"
|
||||
const val WHATS_NEW = "whats_new"
|
||||
const val INTEGRATIONS = "integrations"
|
||||
}
|
||||
|
||||
object NavigationArgs {
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
package co.electriccoin.zcash.ui.common.repository
|
||||
|
||||
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
||||
import co.electriccoin.zcash.ui.common.compose.BalanceState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.model.hasChangePending
|
||||
import co.electriccoin.zcash.ui.common.model.hasValuePending
|
||||
import co.electriccoin.zcash.ui.common.model.spendableBalance
|
||||
import co.electriccoin.zcash.ui.common.model.totalBalance
|
||||
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
interface BalanceRepository {
|
||||
/**
|
||||
* A flow of the wallet balances state used for the UI layer. It's computed from [WalletSnapshot]'s properties
|
||||
* and provides the result [BalanceState] UI state.
|
||||
*/
|
||||
val state: StateFlow<BalanceState>
|
||||
}
|
||||
|
||||
class BalanceRepositoryImpl(
|
||||
walletRepository: WalletRepository,
|
||||
exchangeRateRepository: ExchangeRateRepository
|
||||
) : BalanceRepository {
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
override val state: StateFlow<BalanceState> =
|
||||
combine(
|
||||
walletRepository.walletSnapshot.filterNotNull(),
|
||||
exchangeRateRepository.state,
|
||||
) { snapshot, exchangeRateUsd ->
|
||||
when {
|
||||
// Show the loader only under these conditions:
|
||||
// - Available balance is currently zero AND total balance is non-zero
|
||||
// - And wallet has some ChangePending or ValuePending in progress
|
||||
(
|
||||
snapshot.spendableBalance().value == 0L &&
|
||||
snapshot.totalBalance().value > 0L &&
|
||||
(snapshot.hasChangePending() || snapshot.hasValuePending())
|
||||
) -> {
|
||||
BalanceState.Loading(
|
||||
totalBalance = snapshot.totalBalance(),
|
||||
exchangeRate = exchangeRateUsd
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
BalanceState.Available(
|
||||
totalBalance = snapshot.totalBalance(),
|
||||
spendableBalance = snapshot.spendableBalance(),
|
||||
exchangeRate = exchangeRateUsd
|
||||
)
|
||||
}
|
||||
}
|
||||
}.stateIn(
|
||||
scope,
|
||||
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
BalanceState.None(ExchangeRateState.OptedOut)
|
||||
)
|
||||
}
|
|
@ -0,0 +1,227 @@
|
|||
package co.electriccoin.zcash.ui.common.repository
|
||||
|
||||
import cash.z.ecc.android.sdk.model.ObserveFiatCurrencyResult
|
||||
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
||||
import co.electriccoin.zcash.preference.StandardPreferenceProvider
|
||||
import co.electriccoin.zcash.preference.model.entry.NullableBooleanPreferenceDefault
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.EXCHANGE_RATE_OPT_IN
|
||||
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||
import co.electriccoin.zcash.ui.common.wallet.RefreshLock
|
||||
import co.electriccoin.zcash.ui.common.wallet.StaleLock
|
||||
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
interface ExchangeRateRepository {
|
||||
val navigationCommand: MutableSharedFlow<String>
|
||||
|
||||
val backNavigationCommand: MutableSharedFlow<Unit>
|
||||
|
||||
val isExchangeRateUsdOptedIn: StateFlow<Boolean?>
|
||||
|
||||
val state: StateFlow<ExchangeRateState>
|
||||
|
||||
fun optInExchangeRateUsd(optIn: Boolean)
|
||||
|
||||
fun dismissOptInExchangeRateUsd()
|
||||
|
||||
fun refreshExchangeRateUsd()
|
||||
}
|
||||
|
||||
class ExchangeRateRepositoryImpl(
|
||||
private val walletRepository: WalletRepository,
|
||||
private val standardPreferenceProvider: StandardPreferenceProvider,
|
||||
) : ExchangeRateRepository {
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
override val isExchangeRateUsdOptedIn: StateFlow<Boolean?> =
|
||||
nullableBooleanStateFlow(StandardPreferenceKeys.EXCHANGE_RATE_OPTED_IN)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val exchangeRateUsdInternal =
|
||||
isExchangeRateUsdOptedIn.flatMapLatest { optedIn ->
|
||||
if (optedIn == true) {
|
||||
walletRepository.synchronizer
|
||||
.filterNotNull()
|
||||
.flatMapLatest { synchronizer ->
|
||||
synchronizer.exchangeRateUsd
|
||||
}
|
||||
} else {
|
||||
flowOf(ObserveFiatCurrencyResult(isLoading = false, currencyConversion = null))
|
||||
}
|
||||
}.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.WhileSubscribed(USD_EXCHANGE_REFRESH_LOCK_THRESHOLD),
|
||||
initialValue = ObserveFiatCurrencyResult(isLoading = false, currencyConversion = null)
|
||||
)
|
||||
|
||||
private val usdExchangeRateTimestamp =
|
||||
exchangeRateUsdInternal
|
||||
.map {
|
||||
it.currencyConversion?.timestamp
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
private val refreshExchangeRateUsdLock =
|
||||
RefreshLock(
|
||||
timestampToObserve = usdExchangeRateTimestamp,
|
||||
lockDuration = USD_EXCHANGE_REFRESH_LOCK_THRESHOLD
|
||||
)
|
||||
|
||||
private val staleExchangeRateUsdLock =
|
||||
StaleLock(
|
||||
timestampToObserve = usdExchangeRateTimestamp,
|
||||
lockDuration = USD_EXCHANGE_STALE_LOCK_THRESHOLD,
|
||||
onRefresh = { refreshExchangeRateUsdInternal().join() }
|
||||
)
|
||||
|
||||
private var lastExchangeRateUsdValue: ExchangeRateState = ExchangeRateState.OptedOut
|
||||
|
||||
override val state: StateFlow<ExchangeRateState> =
|
||||
channelFlow {
|
||||
combine(
|
||||
isExchangeRateUsdOptedIn,
|
||||
exchangeRateUsdInternal,
|
||||
staleExchangeRateUsdLock.state,
|
||||
refreshExchangeRateUsdLock.state,
|
||||
) { isOptedIn, exchangeRate, isStale, isRefreshEnabled ->
|
||||
lastExchangeRateUsdValue =
|
||||
when (isOptedIn) {
|
||||
true ->
|
||||
when (val lastValue = lastExchangeRateUsdValue) {
|
||||
is ExchangeRateState.Data ->
|
||||
lastValue.copy(
|
||||
isLoading = exchangeRate.isLoading,
|
||||
isStale = isStale,
|
||||
isRefreshEnabled = isRefreshEnabled,
|
||||
currencyConversion = exchangeRate.currencyConversion,
|
||||
)
|
||||
|
||||
ExchangeRateState.OptedOut ->
|
||||
ExchangeRateState.Data(
|
||||
isLoading = exchangeRate.isLoading,
|
||||
isStale = isStale,
|
||||
isRefreshEnabled = isRefreshEnabled,
|
||||
currencyConversion = exchangeRate.currencyConversion,
|
||||
onRefresh = ::refreshExchangeRateUsd
|
||||
)
|
||||
|
||||
is ExchangeRateState.OptIn ->
|
||||
ExchangeRateState.Data(
|
||||
isLoading = exchangeRate.isLoading,
|
||||
isStale = isStale,
|
||||
isRefreshEnabled = isRefreshEnabled,
|
||||
currencyConversion = exchangeRate.currencyConversion,
|
||||
onRefresh = ::refreshExchangeRateUsd
|
||||
)
|
||||
}
|
||||
|
||||
false -> ExchangeRateState.OptedOut
|
||||
null ->
|
||||
ExchangeRateState.OptIn(
|
||||
onDismissClick = ::dismissWidgetOptInExchangeRateUsd,
|
||||
onPrimaryClick = ::showOptInExchangeRateUsd
|
||||
)
|
||||
}
|
||||
|
||||
lastExchangeRateUsdValue
|
||||
}.distinctUntilChanged()
|
||||
.onEach {
|
||||
Twig.info { "[USD] $it" }
|
||||
send(it)
|
||||
}
|
||||
.launchIn(this)
|
||||
|
||||
awaitClose {
|
||||
// do nothing
|
||||
}
|
||||
}.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = ExchangeRateState.OptedOut
|
||||
)
|
||||
|
||||
override val navigationCommand = MutableSharedFlow<String>()
|
||||
|
||||
override val backNavigationCommand = MutableSharedFlow<Unit>()
|
||||
|
||||
override fun refreshExchangeRateUsd() {
|
||||
refreshExchangeRateUsdInternal()
|
||||
}
|
||||
|
||||
private fun refreshExchangeRateUsdInternal() =
|
||||
scope.launch {
|
||||
val synchronizer = walletRepository.synchronizer.filterNotNull().first()
|
||||
val value = state.value
|
||||
if (value is ExchangeRateState.Data && value.isRefreshEnabled && !value.isLoading) {
|
||||
synchronizer.refreshExchangeRateUsd()
|
||||
}
|
||||
}
|
||||
|
||||
override fun optInExchangeRateUsd(optIn: Boolean) {
|
||||
scope.launch {
|
||||
setNullableBooleanPreference(StandardPreferenceKeys.EXCHANGE_RATE_OPTED_IN, optIn)
|
||||
backNavigationCommand.emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dismissOptInExchangeRateUsd() {
|
||||
scope.launch {
|
||||
setNullableBooleanPreference(StandardPreferenceKeys.EXCHANGE_RATE_OPTED_IN, false)
|
||||
backNavigationCommand.emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismissWidgetOptInExchangeRateUsd() {
|
||||
setNullableBooleanPreference(StandardPreferenceKeys.EXCHANGE_RATE_OPTED_IN, false)
|
||||
}
|
||||
|
||||
private fun showOptInExchangeRateUsd() =
|
||||
scope.launch {
|
||||
navigationCommand.emit(EXCHANGE_RATE_OPT_IN)
|
||||
}
|
||||
|
||||
private fun nullableBooleanStateFlow(default: NullableBooleanPreferenceDefault): StateFlow<Boolean?> =
|
||||
flow {
|
||||
emitAll(default.observe(standardPreferenceProvider()))
|
||||
}.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
initialValue = null
|
||||
)
|
||||
|
||||
private fun setNullableBooleanPreference(
|
||||
default: NullableBooleanPreferenceDefault,
|
||||
newState: Boolean
|
||||
) {
|
||||
scope.launch {
|
||||
default.putValue(standardPreferenceProvider(), newState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val USD_EXCHANGE_REFRESH_LOCK_THRESHOLD = 2.minutes
|
||||
private val USD_EXCHANGE_STALE_LOCK_THRESHOLD = 15.minutes
|
|
@ -1,40 +1,58 @@
|
|||
package co.electriccoin.zcash.ui.common.repository
|
||||
|
||||
import android.app.Application
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.bip39.toSeed
|
||||
import cash.z.ecc.android.sdk.SdkSynchronizer
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.WalletCoordinator
|
||||
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.FastestServersResult
|
||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||
import cash.z.ecc.android.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||
import cash.z.ecc.android.sdk.model.WalletAddresses
|
||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
||||
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||
import co.electriccoin.zcash.preference.EncryptedPreferenceProvider
|
||||
import co.electriccoin.zcash.preference.StandardPreferenceProvider
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.common.extension.throttle
|
||||
import co.electriccoin.zcash.ui.common.model.FastestServersState
|
||||
import co.electriccoin.zcash.ui.common.model.OnboardingState
|
||||
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.provider.GetDefaultServersProvider
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.SecretState
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.SynchronizerError
|
||||
import co.electriccoin.zcash.ui.preference.PersistableWalletPreferenceDefault
|
||||
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
@ -44,13 +62,31 @@ import kotlinx.coroutines.flow.withIndex
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
interface WalletRepository {
|
||||
/**
|
||||
* Synchronizer that is retained long enough to survive configuration changes.
|
||||
*/
|
||||
val synchronizer: StateFlow<Synchronizer?>
|
||||
val secretState: StateFlow<SecretState?>
|
||||
val secretState: StateFlow<SecretState>
|
||||
val fastestServers: StateFlow<FastestServersState>
|
||||
val persistableWallet: Flow<PersistableWallet?>
|
||||
val addresses: StateFlow<WalletAddresses?>
|
||||
val walletSnapshot: StateFlow<WalletSnapshot?>
|
||||
val onboardingState: Flow<OnboardingState>
|
||||
val spendingKey: StateFlow<UnifiedSpendingKey?>
|
||||
|
||||
/**
|
||||
* A flow of the wallet block synchronization state.
|
||||
*/
|
||||
val walletRestoringState: StateFlow<WalletRestoringState>
|
||||
|
||||
/**
|
||||
* A flow of the wallet current state information that should be displayed in screens top app bar.
|
||||
*/
|
||||
val walletStateInformation: StateFlow<TopAppBarSubTitleState>
|
||||
|
||||
fun persistWallet(persistableWallet: PersistableWallet)
|
||||
|
||||
|
@ -82,7 +118,7 @@ class WalletRepositoryImpl(
|
|||
/**
|
||||
* A flow of the wallet onboarding state.
|
||||
*/
|
||||
private val onboardingState =
|
||||
override val onboardingState =
|
||||
flow {
|
||||
emitAll(
|
||||
StandardPreferenceKeys.ONBOARDING_STATE.observe(standardPreferenceProvider()).map { persistedNumber ->
|
||||
|
@ -122,6 +158,26 @@ class WalletRepositoryImpl(
|
|||
initialValue = SecretState.Loading
|
||||
)
|
||||
|
||||
override val spendingKey =
|
||||
secretState
|
||||
.filterIsInstance<SecretState.Ready>()
|
||||
.map { it.persistableWallet }
|
||||
.map {
|
||||
val bip39Seed =
|
||||
withContext(Dispatchers.IO) {
|
||||
Mnemonics.MnemonicCode(it.seedPhrase.joinToString()).toSeed()
|
||||
}
|
||||
DerivationTool.getInstance().deriveUnifiedSpendingKey(
|
||||
seed = bip39Seed,
|
||||
network = it.network,
|
||||
account = Account.DEFAULT
|
||||
)
|
||||
}.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
initialValue = null
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val fastestServers =
|
||||
channelFlow {
|
||||
|
@ -180,6 +236,63 @@ class WalletRepositoryImpl(
|
|||
null
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val walletSnapshot: StateFlow<WalletSnapshot?> =
|
||||
synchronizer
|
||||
.flatMapLatest {
|
||||
if (null == it) {
|
||||
flowOf(null)
|
||||
} else {
|
||||
it.toWalletSnapshot()
|
||||
}
|
||||
}
|
||||
.throttle(1.seconds)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
null
|
||||
)
|
||||
|
||||
/**
|
||||
* A flow of the wallet block synchronization state.
|
||||
*/
|
||||
override val walletRestoringState: StateFlow<WalletRestoringState> =
|
||||
flow {
|
||||
emitAll(
|
||||
StandardPreferenceKeys.WALLET_RESTORING_STATE
|
||||
.observe(standardPreferenceProvider()).map { persistedNumber ->
|
||||
WalletRestoringState.fromNumber(persistedNumber)
|
||||
}
|
||||
)
|
||||
}.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
initialValue = WalletRestoringState.NONE
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val walletStateInformation: StateFlow<TopAppBarSubTitleState> =
|
||||
synchronizer
|
||||
.filterNotNull()
|
||||
.flatMapLatest { synchronizer ->
|
||||
combine(
|
||||
synchronizer.status,
|
||||
walletRestoringState
|
||||
) { status: Synchronizer.Status?, walletRestoringState: WalletRestoringState ->
|
||||
if (Synchronizer.Status.DISCONNECTED == status) {
|
||||
TopAppBarSubTitleState.Disconnected
|
||||
} else if (WalletRestoringState.RESTORING == walletRestoringState) {
|
||||
TopAppBarSubTitleState.Restoring
|
||||
} else {
|
||||
TopAppBarSubTitleState.None
|
||||
}
|
||||
}
|
||||
}.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
initialValue = TopAppBarSubTitleState.None
|
||||
)
|
||||
|
||||
/**
|
||||
* Persists a wallet asynchronously. Clients observe [secretState] to see the side effects.
|
||||
*/
|
||||
|
@ -243,3 +356,74 @@ class WalletRepositoryImpl(
|
|||
|
||||
override suspend fun getSynchronizer(): Synchronizer = synchronizer.filterNotNull().first()
|
||||
}
|
||||
|
||||
private fun Synchronizer.toCommonError(): Flow<SynchronizerError?> =
|
||||
callbackFlow {
|
||||
// just for initial default value emit
|
||||
trySend(null)
|
||||
|
||||
onCriticalErrorHandler = {
|
||||
Twig.error { "WALLET - Error Critical: $it" }
|
||||
trySend(SynchronizerError.Critical(it))
|
||||
false
|
||||
}
|
||||
onProcessorErrorHandler = {
|
||||
Twig.error { "WALLET - Error Processor: $it" }
|
||||
trySend(SynchronizerError.Processor(it))
|
||||
false
|
||||
}
|
||||
onSubmissionErrorHandler = {
|
||||
Twig.error { "WALLET - Error Submission: $it" }
|
||||
trySend(SynchronizerError.Submission(it))
|
||||
false
|
||||
}
|
||||
onSetupErrorHandler = {
|
||||
Twig.error { "WALLET - Error Setup: $it" }
|
||||
trySend(SynchronizerError.Setup(it))
|
||||
false
|
||||
}
|
||||
onChainErrorHandler = { x, y ->
|
||||
Twig.error { "WALLET - Error Chain: $x, $y" }
|
||||
trySend(SynchronizerError.Chain(x, y))
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
// nothing to close here
|
||||
}
|
||||
}
|
||||
|
||||
// No good way around needing magic numbers for the indices
|
||||
@Suppress("MagicNumber")
|
||||
private fun Synchronizer.toWalletSnapshot() =
|
||||
combine(
|
||||
// 0
|
||||
status,
|
||||
// 1
|
||||
processorInfo,
|
||||
// 2
|
||||
orchardBalances,
|
||||
// 3
|
||||
saplingBalances,
|
||||
// 4
|
||||
transparentBalance,
|
||||
// 5
|
||||
progress,
|
||||
// 6
|
||||
toCommonError()
|
||||
) { flows ->
|
||||
val orchardBalance = flows[2] as WalletBalance?
|
||||
val saplingBalance = flows[3] as WalletBalance?
|
||||
val transparentBalance = flows[4] as Zatoshi?
|
||||
|
||||
val progressPercentDecimal = (flows[5] as PercentDecimal)
|
||||
|
||||
WalletSnapshot(
|
||||
status = flows[0] as Synchronizer.Status,
|
||||
processorInfo = flows[1] as CompactBlockProcessor.ProcessorInfo,
|
||||
orchardBalance = orchardBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0), Zatoshi(0)),
|
||||
saplingBalance = saplingBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0), Zatoshi(0)),
|
||||
transparentBalance = transparentBalance ?: Zatoshi(0),
|
||||
progress = progressPercentDecimal,
|
||||
synchronizerError = flows[6] as SynchronizerError?
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class GetSpendingKeyUseCase(
|
||||
private val walletRepository: WalletRepository
|
||||
) {
|
||||
suspend operator fun invoke() = walletRepository.spendingKey.filterNotNull().first()
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.BuildConfig
|
||||
import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider
|
||||
|
||||
class IsCoinbaseAvailableUseCase(
|
||||
private val getVersionInfo: GetVersionInfoProvider,
|
||||
) {
|
||||
operator fun invoke(): Boolean {
|
||||
val versionInfo = getVersionInfo()
|
||||
val isDebug = versionInfo.let { it.isDebuggable && !it.isRunningUnderTestService }
|
||||
return !versionInfo.isTestnet && (BuildConfig.ZCASH_COINBASE_APP_ID.isNotEmpty() || isDebug)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
||||
|
||||
class ObserveWalletStateUseCase(
|
||||
private val walletRepository: WalletRepository
|
||||
) {
|
||||
operator fun invoke() = walletRepository.walletStateInformation
|
||||
}
|
|
@ -5,82 +5,48 @@ import android.app.Application
|
|||
import android.content.Intent
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.bip39.toSeed
|
||||
import cash.z.ecc.android.sdk.SdkSynchronizer
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.WalletCoordinator
|
||||
import cash.z.ecc.android.sdk.WalletInitMode
|
||||
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.ObserveFiatCurrencyResult
|
||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||
import cash.z.ecc.android.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
||||
import cash.z.ecc.android.sdk.model.WalletAddresses
|
||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
||||
import cash.z.ecc.sdk.type.fromResources
|
||||
import co.electriccoin.zcash.preference.EncryptedPreferenceProvider
|
||||
import co.electriccoin.zcash.preference.StandardPreferenceProvider
|
||||
import co.electriccoin.zcash.preference.model.entry.NullableBooleanPreferenceDefault
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.MainActivity
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.EXCHANGE_RATE_OPT_IN
|
||||
import co.electriccoin.zcash.ui.common.compose.BalanceState
|
||||
import co.electriccoin.zcash.ui.common.extension.throttle
|
||||
import co.electriccoin.zcash.ui.common.model.OnboardingState
|
||||
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.common.model.hasChangePending
|
||||
import co.electriccoin.zcash.ui.common.model.hasValuePending
|
||||
import co.electriccoin.zcash.ui.common.model.spendableBalance
|
||||
import co.electriccoin.zcash.ui.common.model.totalBalance
|
||||
import co.electriccoin.zcash.ui.common.provider.GetDefaultServersProvider
|
||||
import co.electriccoin.zcash.ui.common.repository.BalanceRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveSynchronizerUseCase
|
||||
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||
import co.electriccoin.zcash.ui.common.wallet.RefreshLock
|
||||
import co.electriccoin.zcash.ui.common.wallet.StaleLock
|
||||
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
|
||||
import co.electriccoin.zcash.ui.screen.account.ext.TransactionOverviewExt
|
||||
import co.electriccoin.zcash.ui.screen.account.ext.getSortHeight
|
||||
import co.electriccoin.zcash.ui.screen.account.state.TransactionHistorySyncState
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
// To make this more multiplatform compatible, we need to remove the dependency on Context
|
||||
// for loading the preferences.
|
||||
|
@ -89,139 +55,30 @@ import kotlin.time.Duration.Companion.seconds
|
|||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
class WalletViewModel(
|
||||
application: Application,
|
||||
observeSynchronizer: ObserveSynchronizerUseCase,
|
||||
balanceRepository: BalanceRepository,
|
||||
private val walletCoordinator: WalletCoordinator,
|
||||
private val walletRepository: WalletRepository,
|
||||
private val exchangeRateRepository: ExchangeRateRepository,
|
||||
private val encryptedPreferenceProvider: EncryptedPreferenceProvider,
|
||||
private val standardPreferenceProvider: StandardPreferenceProvider,
|
||||
private val getAvailableServers: GetDefaultServersProvider
|
||||
private val getAvailableServers: GetDefaultServersProvider,
|
||||
) : AndroidViewModel(application) {
|
||||
val navigationCommand = MutableSharedFlow<String>()
|
||||
val navigationCommand = exchangeRateRepository.navigationCommand
|
||||
|
||||
val backNavigationCommand = MutableSharedFlow<Unit>()
|
||||
val backNavigationCommand = exchangeRateRepository.backNavigationCommand
|
||||
|
||||
/**
|
||||
* Synchronizer that is retained long enough to survive configuration changes.
|
||||
*/
|
||||
val synchronizer = observeSynchronizer()
|
||||
val synchronizer = walletRepository.synchronizer
|
||||
|
||||
/**
|
||||
* A flow of the wallet block synchronization state.
|
||||
*/
|
||||
val walletRestoringState: StateFlow<WalletRestoringState> =
|
||||
flow {
|
||||
emitAll(
|
||||
StandardPreferenceKeys.WALLET_RESTORING_STATE
|
||||
.observe(standardPreferenceProvider()).map { persistedNumber ->
|
||||
WalletRestoringState.fromNumber(persistedNumber)
|
||||
}
|
||||
)
|
||||
}.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
WalletRestoringState.NONE
|
||||
)
|
||||
val walletRestoringState = walletRepository.walletRestoringState
|
||||
|
||||
/**
|
||||
* A flow of the wallet current state information that should be displayed in screens top app bar.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val walletStateInformation: StateFlow<TopAppBarSubTitleState> =
|
||||
synchronizer
|
||||
.filterNotNull()
|
||||
.flatMapLatest { synchronizer ->
|
||||
combine(
|
||||
synchronizer.status,
|
||||
walletRestoringState
|
||||
) { status: Synchronizer.Status?, walletRestoringState: WalletRestoringState ->
|
||||
if (Synchronizer.Status.DISCONNECTED == status) {
|
||||
TopAppBarSubTitleState.Disconnected
|
||||
} else if (WalletRestoringState.RESTORING == walletRestoringState) {
|
||||
TopAppBarSubTitleState.Restoring
|
||||
} else {
|
||||
TopAppBarSubTitleState.None
|
||||
}
|
||||
}
|
||||
}.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
TopAppBarSubTitleState.None
|
||||
)
|
||||
val walletStateInformation = walletRepository.walletStateInformation
|
||||
|
||||
/**
|
||||
* A flow of the wallet onboarding state.
|
||||
*/
|
||||
private val onboardingState =
|
||||
flow {
|
||||
emitAll(
|
||||
StandardPreferenceKeys.ONBOARDING_STATE
|
||||
.observe(standardPreferenceProvider()).map { persistedNumber ->
|
||||
OnboardingState.fromNumber(persistedNumber)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val secretState: StateFlow<SecretState> =
|
||||
combine(
|
||||
walletCoordinator.persistableWallet,
|
||||
onboardingState
|
||||
) { persistableWallet: PersistableWallet?, onboardingState: OnboardingState ->
|
||||
when {
|
||||
onboardingState == OnboardingState.NONE -> SecretState.None
|
||||
onboardingState == OnboardingState.NEEDS_WARN -> SecretState.NeedsWarning
|
||||
onboardingState == OnboardingState.NEEDS_BACKUP && persistableWallet != null -> {
|
||||
SecretState.NeedsBackup(persistableWallet)
|
||||
}
|
||||
|
||||
onboardingState == OnboardingState.READY && persistableWallet != null -> {
|
||||
SecretState.Ready(persistableWallet)
|
||||
}
|
||||
|
||||
else -> SecretState.None
|
||||
}
|
||||
}.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
SecretState.Loading
|
||||
)
|
||||
val secretState: StateFlow<SecretState> = walletRepository.secretState
|
||||
|
||||
// This needs to be refactored once we support pin lock
|
||||
val spendingKey =
|
||||
secretState
|
||||
.filterIsInstance<SecretState.Ready>()
|
||||
.map { it.persistableWallet }
|
||||
.map {
|
||||
val bip39Seed =
|
||||
withContext(Dispatchers.IO) {
|
||||
Mnemonics.MnemonicCode(it.seedPhrase.joinToString()).toSeed()
|
||||
}
|
||||
DerivationTool.getInstance().deriveUnifiedSpendingKey(
|
||||
seed = bip39Seed,
|
||||
network = it.network,
|
||||
account = Account.DEFAULT
|
||||
)
|
||||
}.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
null
|
||||
)
|
||||
val spendingKey = walletRepository.spendingKey
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val walletSnapshot: StateFlow<WalletSnapshot?> =
|
||||
synchronizer
|
||||
.flatMapLatest {
|
||||
if (null == it) {
|
||||
flowOf(null)
|
||||
} else {
|
||||
it.toWalletSnapshot()
|
||||
}
|
||||
}
|
||||
.throttle(1.seconds)
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
null
|
||||
)
|
||||
val walletSnapshot: StateFlow<WalletSnapshot?> = walletRepository.walletSnapshot
|
||||
|
||||
val addresses: StateFlow<WalletAddresses?> = walletRepository.addresses
|
||||
|
||||
|
@ -277,179 +134,23 @@ class WalletViewModel(
|
|||
initialValue = TransactionHistorySyncState.Loading
|
||||
)
|
||||
|
||||
val isExchangeRateUsdOptedIn = nullableBooleanStateFlow(StandardPreferenceKeys.EXCHANGE_RATE_OPTED_IN)
|
||||
val isExchangeRateUsdOptedIn = exchangeRateRepository.isExchangeRateUsdOptedIn
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val exchangeRateUsdInternal =
|
||||
isExchangeRateUsdOptedIn.flatMapLatest { optedIn ->
|
||||
if (optedIn == true) {
|
||||
synchronizer
|
||||
.filterNotNull()
|
||||
.flatMapLatest { synchronizer ->
|
||||
synchronizer.exchangeRateUsd
|
||||
}
|
||||
} else {
|
||||
flowOf(ObserveFiatCurrencyResult(isLoading = false, currencyConversion = null))
|
||||
}
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(USD_EXCHANGE_REFRESH_LOCK_THRESHOLD),
|
||||
initialValue = ObserveFiatCurrencyResult(isLoading = false, currencyConversion = null)
|
||||
)
|
||||
val exchangeRateUsd = exchangeRateRepository.state
|
||||
|
||||
private val usdExchangeRateTimestamp =
|
||||
exchangeRateUsdInternal
|
||||
.map {
|
||||
it.currencyConversion?.timestamp
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
val balanceState = balanceRepository.state
|
||||
|
||||
private var lastExchangeRateUsdValue: ExchangeRateState = ExchangeRateState.OptedOut
|
||||
|
||||
val exchangeRateUsd: StateFlow<ExchangeRateState> =
|
||||
channelFlow {
|
||||
combine(
|
||||
isExchangeRateUsdOptedIn,
|
||||
exchangeRateUsdInternal,
|
||||
staleExchangeRateUsdLock.state,
|
||||
refreshExchangeRateUsdLock.state,
|
||||
) { isOptedIn, exchangeRate, isStale, isRefreshEnabled ->
|
||||
lastExchangeRateUsdValue =
|
||||
when (isOptedIn) {
|
||||
true ->
|
||||
when (val lastValue = lastExchangeRateUsdValue) {
|
||||
is ExchangeRateState.Data ->
|
||||
lastValue.copy(
|
||||
isLoading = exchangeRate.isLoading,
|
||||
isStale = isStale,
|
||||
isRefreshEnabled = isRefreshEnabled,
|
||||
currencyConversion = exchangeRate.currencyConversion,
|
||||
)
|
||||
|
||||
ExchangeRateState.OptedOut ->
|
||||
ExchangeRateState.Data(
|
||||
isLoading = exchangeRate.isLoading,
|
||||
isStale = isStale,
|
||||
isRefreshEnabled = isRefreshEnabled,
|
||||
currencyConversion = exchangeRate.currencyConversion,
|
||||
onRefresh = ::refreshExchangeRateUsd
|
||||
)
|
||||
|
||||
is ExchangeRateState.OptIn ->
|
||||
ExchangeRateState.Data(
|
||||
isLoading = exchangeRate.isLoading,
|
||||
isStale = isStale,
|
||||
isRefreshEnabled = isRefreshEnabled,
|
||||
currencyConversion = exchangeRate.currencyConversion,
|
||||
onRefresh = ::refreshExchangeRateUsd
|
||||
)
|
||||
}
|
||||
|
||||
false -> ExchangeRateState.OptedOut
|
||||
null ->
|
||||
ExchangeRateState.OptIn(
|
||||
onDismissClick = ::dismissWidgetOptInExchangeRateUsd,
|
||||
onPrimaryClick = ::showOptInExchangeRateUsd
|
||||
)
|
||||
}
|
||||
|
||||
lastExchangeRateUsdValue
|
||||
}.distinctUntilChanged()
|
||||
.onEach {
|
||||
Twig.info { "[USD] $it" }
|
||||
send(it)
|
||||
}
|
||||
.launchIn(this)
|
||||
|
||||
awaitClose {
|
||||
// do nothing
|
||||
}
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = ExchangeRateState.OptedOut
|
||||
)
|
||||
|
||||
/**
|
||||
* A flow of the wallet balances state used for the UI layer. It's computed form [WalletSnapshot]'s properties
|
||||
* and provides the result [BalanceState] UI state.
|
||||
*/
|
||||
val balanceState: StateFlow<BalanceState> =
|
||||
combine(
|
||||
walletSnapshot.filterNotNull(),
|
||||
exchangeRateUsd,
|
||||
) { snapshot, exchangeRateUsd ->
|
||||
when {
|
||||
// Show the loader only under these conditions:
|
||||
// - Available balance is currently zero AND total balance is non-zero
|
||||
// - And wallet has some ChangePending or ValuePending in progress
|
||||
(
|
||||
snapshot.spendableBalance().value == 0L &&
|
||||
snapshot.totalBalance().value > 0L &&
|
||||
(snapshot.hasChangePending() || snapshot.hasValuePending())
|
||||
) -> {
|
||||
BalanceState.Loading(
|
||||
totalBalance = snapshot.totalBalance(),
|
||||
exchangeRate = exchangeRateUsd
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
BalanceState.Available(
|
||||
totalBalance = snapshot.totalBalance(),
|
||||
spendableBalance = snapshot.spendableBalance(),
|
||||
exchangeRate = exchangeRateUsd
|
||||
)
|
||||
}
|
||||
}
|
||||
}.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
BalanceState.None(ExchangeRateState.OptedOut)
|
||||
)
|
||||
|
||||
private val refreshExchangeRateUsdLock =
|
||||
RefreshLock(
|
||||
timestampToObserve = usdExchangeRateTimestamp,
|
||||
lockDuration = USD_EXCHANGE_REFRESH_LOCK_THRESHOLD
|
||||
)
|
||||
|
||||
private val staleExchangeRateUsdLock =
|
||||
StaleLock(
|
||||
timestampToObserve = usdExchangeRateTimestamp,
|
||||
lockDuration = USD_EXCHANGE_STALE_LOCK_THRESHOLD,
|
||||
onRefresh = { refreshExchangeRateUsd().join() }
|
||||
)
|
||||
|
||||
fun refreshExchangeRateUsd() =
|
||||
viewModelScope.launch {
|
||||
val synchronizer = synchronizer.filterNotNull().first()
|
||||
val value = exchangeRateUsd.value
|
||||
if (value is ExchangeRateState.Data && value.isRefreshEnabled && !value.isLoading) {
|
||||
synchronizer.refreshExchangeRateUsd()
|
||||
}
|
||||
}
|
||||
|
||||
fun optInExchangeRateUsd(optIn: Boolean) =
|
||||
viewModelScope.launch {
|
||||
setNullableBooleanPreference(StandardPreferenceKeys.EXCHANGE_RATE_OPTED_IN, optIn)
|
||||
backNavigationCommand.emit(Unit)
|
||||
}
|
||||
|
||||
fun dismissOptInExchangeRateUsd() =
|
||||
viewModelScope.launch {
|
||||
setNullableBooleanPreference(StandardPreferenceKeys.EXCHANGE_RATE_OPTED_IN, false)
|
||||
backNavigationCommand.emit(Unit)
|
||||
}
|
||||
|
||||
private fun dismissWidgetOptInExchangeRateUsd() {
|
||||
setNullableBooleanPreference(StandardPreferenceKeys.EXCHANGE_RATE_OPTED_IN, false)
|
||||
fun refreshExchangeRateUsd() {
|
||||
exchangeRateRepository.refreshExchangeRateUsd()
|
||||
}
|
||||
|
||||
private fun showOptInExchangeRateUsd() =
|
||||
viewModelScope.launch {
|
||||
navigationCommand.emit(EXCHANGE_RATE_OPT_IN)
|
||||
}
|
||||
fun optInExchangeRateUsd(optIn: Boolean) {
|
||||
exchangeRateRepository.optInExchangeRateUsd(optIn)
|
||||
}
|
||||
|
||||
fun dismissOptInExchangeRateUsd() {
|
||||
exchangeRateRepository.dismissOptInExchangeRateUsd()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a wallet asynchronously and then persists it. Clients observe
|
||||
|
@ -574,24 +275,6 @@ class WalletViewModel(
|
|||
// Nothing to close
|
||||
}
|
||||
}
|
||||
|
||||
private fun nullableBooleanStateFlow(default: NullableBooleanPreferenceDefault): StateFlow<Boolean?> =
|
||||
flow {
|
||||
emitAll(default.observe(standardPreferenceProvider()))
|
||||
}.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
null
|
||||
)
|
||||
|
||||
private fun setNullableBooleanPreference(
|
||||
default: NullableBooleanPreferenceDefault,
|
||||
newState: Boolean
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
default.putValue(standardPreferenceProvider(), newState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -685,80 +368,6 @@ sealed class SynchronizerError {
|
|||
}
|
||||
}
|
||||
|
||||
private fun Synchronizer.toCommonError(): Flow<SynchronizerError?> =
|
||||
callbackFlow {
|
||||
// just for initial default value emit
|
||||
trySend(null)
|
||||
|
||||
onCriticalErrorHandler = {
|
||||
Twig.error { "WALLET - Error Critical: $it" }
|
||||
trySend(SynchronizerError.Critical(it))
|
||||
false
|
||||
}
|
||||
onProcessorErrorHandler = {
|
||||
Twig.error { "WALLET - Error Processor: $it" }
|
||||
trySend(SynchronizerError.Processor(it))
|
||||
false
|
||||
}
|
||||
onSubmissionErrorHandler = {
|
||||
Twig.error { "WALLET - Error Submission: $it" }
|
||||
trySend(SynchronizerError.Submission(it))
|
||||
false
|
||||
}
|
||||
onSetupErrorHandler = {
|
||||
Twig.error { "WALLET - Error Setup: $it" }
|
||||
trySend(SynchronizerError.Setup(it))
|
||||
false
|
||||
}
|
||||
onChainErrorHandler = { x, y ->
|
||||
Twig.error { "WALLET - Error Chain: $x, $y" }
|
||||
trySend(SynchronizerError.Chain(x, y))
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
// nothing to close here
|
||||
}
|
||||
}
|
||||
|
||||
// No good way around needing magic numbers for the indices
|
||||
@Suppress("MagicNumber")
|
||||
private fun Synchronizer.toWalletSnapshot() =
|
||||
combine(
|
||||
// 0
|
||||
status,
|
||||
// 1
|
||||
processorInfo,
|
||||
// 2
|
||||
orchardBalances,
|
||||
// 3
|
||||
saplingBalances,
|
||||
// 4
|
||||
transparentBalance,
|
||||
// 5
|
||||
progress,
|
||||
// 6
|
||||
toCommonError()
|
||||
) { flows ->
|
||||
val orchardBalance = flows[2] as WalletBalance?
|
||||
val saplingBalance = flows[3] as WalletBalance?
|
||||
val transparentBalance = flows[4] as Zatoshi?
|
||||
|
||||
val progressPercentDecimal = (flows[5] as PercentDecimal)
|
||||
|
||||
WalletSnapshot(
|
||||
status = flows[0] as Synchronizer.Status,
|
||||
processorInfo = flows[1] as CompactBlockProcessor.ProcessorInfo,
|
||||
orchardBalance = orchardBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0), Zatoshi(0)),
|
||||
saplingBalance = saplingBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0), Zatoshi(0)),
|
||||
transparentBalance = transparentBalance ?: Zatoshi(0),
|
||||
progress = progressPercentDecimal,
|
||||
synchronizerError = flows[6] as SynchronizerError?
|
||||
)
|
||||
}
|
||||
|
||||
fun Synchronizer.Status.isSyncing() = this == Synchronizer.Status.SYNCING
|
||||
|
||||
fun Synchronizer.Status.isSynced() = this == Synchronizer.Status.SYNCED
|
||||
|
||||
private val USD_EXCHANGE_REFRESH_LOCK_THRESHOLD = 2.minutes
|
||||
private val USD_EXCHANGE_STALE_LOCK_THRESHOLD = 15.minutes
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package co.electriccoin.zcash.ui.screen.advancedsettings
|
||||
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItemState
|
||||
|
||||
data class AdvancedSettingsState(
|
||||
val onBack: () -> Unit,
|
||||
val onRecoveryPhraseClick: () -> Unit,
|
||||
|
@ -9,5 +7,4 @@ data class AdvancedSettingsState(
|
|||
val onChooseServerClick: () -> Unit,
|
||||
val onCurrencyConversionClick: () -> Unit,
|
||||
val onDeleteZashiClick: () -> Unit,
|
||||
val coinbaseButton: ZashiSettingsListItemState?,
|
||||
)
|
||||
|
|
|
@ -2,14 +2,11 @@
|
|||
|
||||
package co.electriccoin.zcash.ui.screen.advancedsettings
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.electriccoin.zcash.di.koinActivityViewModel
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalActivity
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalNavController
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.screen.advancedsettings.view.AdvancedSettings
|
||||
|
@ -23,7 +20,6 @@ internal fun WrapAdvancedSettings(
|
|||
goExportPrivateData: () -> Unit,
|
||||
goSeedRecovery: () -> Unit,
|
||||
) {
|
||||
val activity = LocalActivity.current
|
||||
val navController = LocalNavController.current
|
||||
val walletViewModel = koinActivityViewModel<WalletViewModel>()
|
||||
val viewModel = koinViewModel<AdvancedSettingsViewModel>()
|
||||
|
@ -45,18 +41,6 @@ internal fun WrapAdvancedSettings(
|
|||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.coinbaseNavigationCommand.collect { uri ->
|
||||
val intent =
|
||||
CustomTabsIntent.Builder()
|
||||
.setUrlBarHidingEnabled(true)
|
||||
.setShowTitle(true)
|
||||
.setShareState(CustomTabsIntent.SHARE_STATE_OFF)
|
||||
.build()
|
||||
intent.launchUrl(activity, Uri.parse(uri))
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.backNavigationCommand.collect {
|
||||
navController.popBackStack()
|
||||
|
|
|
@ -29,14 +29,12 @@ import co.electriccoin.zcash.ui.design.component.ZashiButton
|
|||
import co.electriccoin.zcash.ui.design.component.ZashiButtonDefaults
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiHorizontalDivider
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItem
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItemState
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarBackNavigation
|
||||
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||
import co.electriccoin.zcash.ui.design.util.orDark
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.advancedsettings.AdvancedSettingsState
|
||||
import co.electriccoin.zcash.ui.screen.advancedsettings.AdvancedSettingsTag
|
||||
|
||||
|
@ -70,13 +68,13 @@ fun AdvancedSettings(
|
|||
) {
|
||||
ZashiSettingsListItem(
|
||||
text = stringResource(id = R.string.advanced_settings_recovery),
|
||||
icon = R.drawable.ic_advanced_settings_recovery orDark R.drawable.ic_advanced_settings_recovery_dark,
|
||||
icon = R.drawable.ic_advanced_settings_recovery,
|
||||
onClick = state.onRecoveryPhraseClick
|
||||
)
|
||||
ZashiHorizontalDivider()
|
||||
ZashiSettingsListItem(
|
||||
text = stringResource(id = R.string.advanced_settings_export),
|
||||
icon = R.drawable.ic_advanced_settings_export orDark R.drawable.ic_advanced_settings_export_dark,
|
||||
icon = R.drawable.ic_advanced_settings_export,
|
||||
onClick = state.onExportPrivateDataClick
|
||||
)
|
||||
ZashiHorizontalDivider()
|
||||
|
@ -84,7 +82,7 @@ fun AdvancedSettings(
|
|||
text = stringResource(id = R.string.advanced_settings_choose_server),
|
||||
icon =
|
||||
R.drawable.ic_advanced_settings_choose_server orDark
|
||||
R.drawable.ic_advanced_settings_choose_server_dark,
|
||||
R.drawable.ic_advanced_settings_choose_server,
|
||||
onClick = state.onChooseServerClick
|
||||
)
|
||||
ZashiHorizontalDivider()
|
||||
|
@ -92,16 +90,9 @@ fun AdvancedSettings(
|
|||
text = stringResource(id = R.string.advanced_settings_currency_conversion),
|
||||
icon =
|
||||
R.drawable.ic_advanced_settings_currency_conversion orDark
|
||||
R.drawable.ic_advanced_settings_currency_conversion_dark,
|
||||
R.drawable.ic_advanced_settings_currency_conversion,
|
||||
onClick = state.onCurrencyConversionClick
|
||||
)
|
||||
if (state.coinbaseButton != null) {
|
||||
ZashiHorizontalDivider()
|
||||
ZashiSettingsListItem(
|
||||
icon = R.drawable.ic_advanced_settings_coinbase,
|
||||
state = state.coinbaseButton
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Row(
|
||||
|
@ -171,11 +162,6 @@ private fun AdvancedSettingsPreview() =
|
|||
onChooseServerClick = {},
|
||||
onCurrencyConversionClick = {},
|
||||
onDeleteZashiClick = {},
|
||||
coinbaseButton =
|
||||
ZashiSettingsListItemState(
|
||||
text = stringRes("Coinbase"),
|
||||
onClick = {}
|
||||
)
|
||||
),
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
)
|
||||
|
|
|
@ -2,27 +2,14 @@ package co.electriccoin.zcash.ui.screen.advancedsettings.viewmodel
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import co.electriccoin.zcash.ui.BuildConfig
|
||||
import co.electriccoin.zcash.ui.NavigationTargets
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.GetZcashCurrencyProvider
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetTransparentAddressUseCase
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItemState
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.advancedsettings.AdvancedSettingsState
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AdvancedSettingsViewModel(
|
||||
getVersionInfo: GetVersionInfoProvider,
|
||||
getZcashCurrency: GetZcashCurrencyProvider,
|
||||
private val getTransparentAddress: GetTransparentAddressUseCase,
|
||||
) : ViewModel() {
|
||||
private val forceShowCoinbaseForDebug = getVersionInfo().let { it.isDebuggable && !it.isRunningUnderTestService }
|
||||
|
||||
class AdvancedSettingsViewModel : ViewModel() {
|
||||
val state =
|
||||
MutableStateFlow(
|
||||
AdvancedSettingsState(
|
||||
|
@ -32,22 +19,11 @@ class AdvancedSettingsViewModel(
|
|||
onChooseServerClick = ::onChooseServerClick,
|
||||
onCurrencyConversionClick = ::onCurrencyConversionClick,
|
||||
onDeleteZashiClick = {},
|
||||
coinbaseButton =
|
||||
ZashiSettingsListItemState(
|
||||
// Set the wallet currency by app build is more future-proof, although we hide it from the UI
|
||||
// in the Testnet build
|
||||
text = stringRes(R.string.advanced_settings_coinbase, getZcashCurrency.getLocalizedName()),
|
||||
onClick = { onBuyWithCoinbaseClicked() }
|
||||
).takeIf {
|
||||
!getVersionInfo().isTestnet &&
|
||||
(BuildConfig.ZCASH_COINBASE_APP_ID.isNotEmpty() || forceShowCoinbaseForDebug)
|
||||
}
|
||||
)
|
||||
).asStateFlow()
|
||||
|
||||
val navigationCommand = MutableSharedFlow<String>()
|
||||
val backNavigationCommand = MutableSharedFlow<Unit>()
|
||||
val coinbaseNavigationCommand = MutableSharedFlow<String>()
|
||||
|
||||
private fun onChooseServerClick() =
|
||||
viewModelScope.launch {
|
||||
|
@ -59,32 +35,6 @@ class AdvancedSettingsViewModel(
|
|||
navigationCommand.emit(NavigationTargets.SETTINGS_EXCHANGE_RATE_OPT_IN)
|
||||
}
|
||||
|
||||
private fun onBuyWithCoinbaseClicked() {
|
||||
viewModelScope.launch {
|
||||
val appId = BuildConfig.ZCASH_COINBASE_APP_ID
|
||||
|
||||
when {
|
||||
appId.isEmpty() && forceShowCoinbaseForDebug ->
|
||||
coinbaseNavigationCommand.emit("https://www.coinbase.com") // fallback debug url
|
||||
|
||||
appId.isEmpty() && forceShowCoinbaseForDebug -> {
|
||||
// should not happen
|
||||
}
|
||||
|
||||
appId.isNotEmpty() -> {
|
||||
val address = getTransparentAddress().address
|
||||
val url =
|
||||
"https://pay.coinbase.com/buy/select-asset?appId=$appId&addresses={\"${address}\":[\"zcash\"]}"
|
||||
coinbaseNavigationCommand.emit(url)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// should not happen
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onBack() =
|
||||
viewModelScope.launch {
|
||||
backNavigationCommand.emit(Unit)
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
package co.electriccoin.zcash.ui.screen.integrations
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.electriccoin.zcash.di.koinActivityViewModel
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalActivity
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalNavController
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.screen.integrations.view.Integrations
|
||||
import co.electriccoin.zcash.ui.screen.integrations.viewmodel.IntegrationsViewModel
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@Composable
|
||||
internal fun WrapIntegrations() {
|
||||
val activity = LocalActivity.current
|
||||
val navController = LocalNavController.current
|
||||
val walletViewModel = koinActivityViewModel<WalletViewModel>()
|
||||
val viewModel = koinViewModel<IntegrationsViewModel>()
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.backNavigationCommand.collect {
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.coinbaseNavigationCommand.collect { uri ->
|
||||
val intent =
|
||||
CustomTabsIntent.Builder()
|
||||
.setUrlBarHidingEnabled(true)
|
||||
.setShowTitle(true)
|
||||
.setShareState(CustomTabsIntent.SHARE_STATE_OFF)
|
||||
.build()
|
||||
intent.launchUrl(activity, Uri.parse(uri))
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
viewModel.onBack()
|
||||
}
|
||||
|
||||
state?.let {
|
||||
Integrations(
|
||||
state = it,
|
||||
topAppBarSubTitleState = walletState,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package co.electriccoin.zcash.ui.screen.integrations.model
|
||||
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItemState
|
||||
import co.electriccoin.zcash.ui.design.util.StringResource
|
||||
|
||||
data class IntegrationsState(
|
||||
val version: StringResource,
|
||||
val coinbase: ZashiSettingsListItemState?,
|
||||
val disabledInfo: StringResource?,
|
||||
val onBack: () -> Unit,
|
||||
)
|
|
@ -0,0 +1,149 @@
|
|||
package co.electriccoin.zcash.ui.screen.integrations.view
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
||||
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItem
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItemState
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarBackNavigation
|
||||
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||
import co.electriccoin.zcash.ui.design.util.getValue
|
||||
import co.electriccoin.zcash.ui.design.util.orDark
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.integrations.model.IntegrationsState
|
||||
import co.electriccoin.zcash.ui.screen.settings.SettingsTag
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun Integrations(
|
||||
state: IntegrationsState,
|
||||
topAppBarSubTitleState: TopAppBarSubTitleState
|
||||
) {
|
||||
BlankBgScaffold(
|
||||
topBar = {
|
||||
IntegrationsTopAppBar(onBack = state.onBack, subTitleState = topAppBarSubTitleState)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = paddingValues.calculateBottomPadding(),
|
||||
start = 4.dp,
|
||||
end = 4.dp
|
||||
),
|
||||
) {
|
||||
state.coinbase?.let {
|
||||
ZashiSettingsListItem(state = it)
|
||||
}
|
||||
|
||||
state.disabledInfo?.let {
|
||||
Spacer(modifier = Modifier.height(28.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_advanced_settings_info),
|
||||
contentDescription = "",
|
||||
colorFilter = ColorFilter.tint(ZashiColors.Utility.WarningYellow.utilityOrange700)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = it.getValue(),
|
||||
fontSize = 12.sp,
|
||||
color = ZashiColors.Utility.WarningYellow.utilityOrange700,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingMin))
|
||||
Image(
|
||||
modifier = Modifier.align(CenterHorizontally),
|
||||
painter =
|
||||
painterResource(id = R.drawable.ic_settings_zashi orDark R.drawable.ic_settings_zashi),
|
||||
contentDescription = ""
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
modifier = Modifier.align(CenterHorizontally),
|
||||
text = state.version.getValue(),
|
||||
color = ZashiColors.Text.textTertiary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IntegrationsTopAppBar(
|
||||
onBack: () -> Unit,
|
||||
subTitleState: TopAppBarSubTitleState
|
||||
) {
|
||||
ZashiSmallTopAppBar(
|
||||
title = stringResource(id = R.string.integrations_title),
|
||||
subtitle =
|
||||
when (subTitleState) {
|
||||
TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label)
|
||||
TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label)
|
||||
TopAppBarSubTitleState.None -> null
|
||||
},
|
||||
modifier = Modifier.testTag(SettingsTag.SETTINGS_TOP_APP_BAR),
|
||||
showTitleLogo = true,
|
||||
navigationAction = {
|
||||
ZashiTopAppBarBackNavigation(onBack = onBack)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun IntegrationSettings() {
|
||||
ZcashTheme {
|
||||
Integrations(
|
||||
state =
|
||||
IntegrationsState(
|
||||
version = stringRes("Version 1.2"),
|
||||
onBack = {},
|
||||
coinbase =
|
||||
ZashiSettingsListItemState(
|
||||
icon = R.drawable.ic_integrations_coinbase,
|
||||
text = stringRes("Coinbase"),
|
||||
subtitle = stringRes("Coinbase subtitle"),
|
||||
) {},
|
||||
disabledInfo = stringRes("Disabled info"),
|
||||
),
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package co.electriccoin.zcash.ui.screen.integrations.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
||||
import co.electriccoin.zcash.ui.BuildConfig
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
||||
import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.GetZcashCurrencyProvider
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetTransparentAddressUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.IsCoinbaseAvailableUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveWalletStateUseCase
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItemState
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.integrations.model.IntegrationsState
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class IntegrationsViewModel(
|
||||
getVersionInfo: GetVersionInfoProvider,
|
||||
getZcashCurrency: GetZcashCurrencyProvider,
|
||||
observeWalletState: ObserveWalletStateUseCase,
|
||||
private val getTransparentAddress: GetTransparentAddressUseCase,
|
||||
private val isCoinbaseAvailable: IsCoinbaseAvailableUseCase,
|
||||
) : ViewModel() {
|
||||
val backNavigationCommand = MutableSharedFlow<Unit>()
|
||||
val coinbaseNavigationCommand = MutableSharedFlow<String>()
|
||||
|
||||
private val versionInfo = getVersionInfo()
|
||||
private val isDebug = versionInfo.let { it.isDebuggable && !it.isRunningUnderTestService }
|
||||
|
||||
private val isEnabled =
|
||||
observeWalletState()
|
||||
.map {
|
||||
it != TopAppBarSubTitleState.Restoring
|
||||
}
|
||||
|
||||
val state =
|
||||
isEnabled.map { isEnabled ->
|
||||
IntegrationsState(
|
||||
version = stringRes(R.string.integrations_version, versionInfo.versionName),
|
||||
coinbase =
|
||||
ZashiSettingsListItemState(
|
||||
// Set the wallet currency by app build is more future-proof, although we hide it from the UI
|
||||
// in the Testnet build
|
||||
icon = R.drawable.ic_integrations_coinbase,
|
||||
text = stringRes(R.string.integrations_coinbase, getZcashCurrency.getLocalizedName()),
|
||||
subtitle =
|
||||
stringRes(
|
||||
R.string.integrations_coinbase_subtitle,
|
||||
getZcashCurrency.getLocalizedName()
|
||||
),
|
||||
onClick = ::onBuyWithCoinbaseClicked
|
||||
).takeIf { isCoinbaseAvailable() },
|
||||
disabledInfo = stringRes(R.string.integrations_disabled_info).takeIf { isEnabled.not() },
|
||||
onBack = ::onBack,
|
||||
)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
initialValue = null
|
||||
)
|
||||
|
||||
fun onBack() =
|
||||
viewModelScope.launch {
|
||||
backNavigationCommand.emit(Unit)
|
||||
}
|
||||
|
||||
private fun onBuyWithCoinbaseClicked() =
|
||||
viewModelScope.launch {
|
||||
val appId = BuildConfig.ZCASH_COINBASE_APP_ID
|
||||
|
||||
when {
|
||||
appId.isEmpty() && isDebug ->
|
||||
coinbaseNavigationCommand.emit("https://www.coinbase.com") // fallback debug url
|
||||
|
||||
appId.isEmpty() && isDebug -> {
|
||||
// should not happen
|
||||
}
|
||||
|
||||
appId.isNotEmpty() -> {
|
||||
val address = getTransparentAddress().address
|
||||
val url =
|
||||
"https://pay.coinbase.com/buy/select-asset?appId=$appId&addresses={\"${address}\":[\"zcash\"]}"
|
||||
coinbaseNavigationCommand.emit(url)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// should not happen
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package co.electriccoin.zcash.ui.screen.settings.model
|
||||
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItemState
|
||||
import co.electriccoin.zcash.ui.design.util.StringResource
|
||||
|
||||
data class SettingsState(
|
||||
|
@ -8,6 +9,7 @@ data class SettingsState(
|
|||
val settingsTroubleshootingState: SettingsTroubleshootingState?,
|
||||
val onAddressBookClick: () -> Unit,
|
||||
val onBack: () -> Unit,
|
||||
val integrations: ZashiSettingsListItemState,
|
||||
val onAdvancedSettingsClick: () -> Unit,
|
||||
val onAboutUsClick: () -> Unit,
|
||||
val onSendUsFeedbackClick: () -> Unit,
|
||||
|
|
|
@ -34,17 +34,18 @@ import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
|
|||
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiHorizontalDivider
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItem
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItemState
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarBackNavigation
|
||||
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||
import co.electriccoin.zcash.ui.design.util.getValue
|
||||
import co.electriccoin.zcash.ui.design.util.orDark
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.settings.SettingsTag
|
||||
import co.electriccoin.zcash.ui.screen.settings.model.SettingsState
|
||||
import co.electriccoin.zcash.ui.screen.settings.model.SettingsTroubleshootingState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
|
@ -82,21 +83,23 @@ fun Settings(
|
|||
onClick = state.onAddressBookClick
|
||||
)
|
||||
ZashiHorizontalDivider()
|
||||
ZashiSettingsListItem(state = state.integrations)
|
||||
ZashiHorizontalDivider()
|
||||
ZashiSettingsListItem(
|
||||
text = stringResource(id = R.string.settings_advanced_settings),
|
||||
icon = R.drawable.ic_advanced_settings orDark R.drawable.ic_advanced_settings_dark,
|
||||
icon = R.drawable.ic_advanced_settings,
|
||||
onClick = state.onAdvancedSettingsClick
|
||||
)
|
||||
ZashiHorizontalDivider()
|
||||
ZashiSettingsListItem(
|
||||
text = stringResource(id = R.string.settings_about_us),
|
||||
icon = R.drawable.ic_settings_info orDark R.drawable.ic_settings_info_dark,
|
||||
icon = R.drawable.ic_settings_info,
|
||||
onClick = state.onAboutUsClick
|
||||
)
|
||||
ZashiHorizontalDivider()
|
||||
ZashiSettingsListItem(
|
||||
text = stringResource(id = R.string.settings_feedback),
|
||||
icon = R.drawable.ic_settings_feedback orDark R.drawable.ic_settings_feedback_dark,
|
||||
icon = R.drawable.ic_settings_feedback,
|
||||
onClick = state.onSendUsFeedbackClick
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
@ -104,7 +107,7 @@ fun Settings(
|
|||
Image(
|
||||
modifier = Modifier.align(CenterHorizontally),
|
||||
painter =
|
||||
painterResource(id = R.drawable.ic_settings_zashi orDark R.drawable.ic_settings_zashi_dark),
|
||||
painterResource(id = R.drawable.ic_settings_zashi),
|
||||
contentDescription = ""
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
@ -236,7 +239,13 @@ private fun PreviewSettings() {
|
|||
onAdvancedSettingsClick = {},
|
||||
onAboutUsClick = {},
|
||||
onSendUsFeedbackClick = {},
|
||||
onAddressBookClick = {}
|
||||
onAddressBookClick = {},
|
||||
integrations =
|
||||
ZashiSettingsListItemState(
|
||||
icon = R.drawable.ic_settings_integrations,
|
||||
text = stringRes("Integrations"),
|
||||
titleIcons = persistentListOf(R.drawable.ic_integrations_coinbase)
|
||||
) {}
|
||||
),
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
)
|
||||
|
@ -258,7 +267,13 @@ private fun PreviewSettingsLoading() {
|
|||
onAdvancedSettingsClick = {},
|
||||
onAboutUsClick = {},
|
||||
onSendUsFeedbackClick = {},
|
||||
onAddressBookClick = {}
|
||||
onAddressBookClick = {},
|
||||
integrations =
|
||||
ZashiSettingsListItemState(
|
||||
icon = R.drawable.ic_settings_integrations,
|
||||
text = stringRes("Integrations"),
|
||||
titleIcons = persistentListOf(R.drawable.ic_integrations_coinbase)
|
||||
) {}
|
||||
),
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
)
|
||||
|
|
|
@ -7,18 +7,21 @@ import co.electriccoin.zcash.preference.StandardPreferenceProvider
|
|||
import co.electriccoin.zcash.preference.model.entry.BooleanPreferenceDefault
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.ABOUT
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.ADVANCED_SETTINGS
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.INTEGRATIONS
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.RescanBlockchainUseCase
|
||||
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItemState
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
|
||||
import co.electriccoin.zcash.ui.screen.addressbook.AddressBookArgs
|
||||
import co.electriccoin.zcash.ui.screen.settings.model.SettingsState
|
||||
import co.electriccoin.zcash.ui.screen.settings.model.SettingsTroubleshootingState
|
||||
import co.electriccoin.zcash.ui.screen.settings.model.TroubleshootingItemState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
@ -99,6 +102,13 @@ class SettingsViewModel(
|
|||
version = stringRes(R.string.settings_version, versionInfo.versionName),
|
||||
settingsTroubleshootingState = troubleshootingState,
|
||||
onBack = ::onBack,
|
||||
integrations =
|
||||
ZashiSettingsListItemState(
|
||||
text = stringRes(R.string.settings_integrations),
|
||||
icon = R.drawable.ic_settings_integrations,
|
||||
onClick = ::onIntegrationsClick,
|
||||
titleIcons = persistentListOf(R.drawable.ic_integrations_coinbase)
|
||||
),
|
||||
onAdvancedSettingsClick = ::onAdvancedSettingsClick,
|
||||
onAboutUsClick = ::onAboutUsClick,
|
||||
onSendUsFeedbackClick = ::onSendUsFeedbackClick,
|
||||
|
@ -140,6 +150,11 @@ class SettingsViewModel(
|
|||
backNavigationCommand.emit(Unit)
|
||||
}
|
||||
|
||||
private fun onIntegrationsClick() =
|
||||
viewModelScope.launch {
|
||||
navigationCommand.emit(INTEGRATIONS)
|
||||
}
|
||||
|
||||
private fun onAdvancedSettingsClick() =
|
||||
viewModelScope.launch {
|
||||
navigationCommand.emit(ADVANCED_SETTINGS)
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
<string name="advanced_settings_export">Export Private Data</string>
|
||||
<string name="advanced_settings_choose_server">Choose a Server</string>
|
||||
<string name="advanced_settings_currency_conversion">Currency Conversion</string>
|
||||
<string name="advanced_settings_coinbase">Buy <xliff:g id="currency" example="ZEC">%1$s</xliff:g> with Coinbase</string>
|
||||
<string name="advanced_settings_info">You will be asked to confirm on the next screen</string>
|
||||
<string name="advanced_settings_delete_button">Delete Zashi</string>
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="integrations_title">Integrations</string>
|
||||
<string name="integrations_coinbase">Buy <xliff:g id="currency" example="ZEC">%1$s</xliff:g> with Coinbase</string>
|
||||
<string name="integrations_coinbase_subtitle">A hassle-free way to buy <xliff:g id="currency" example="ZEC">%1$s</xliff:g> and get it directly into your Zashi wallet.</string>
|
||||
<string name="integrations_version">Version %s</string>
|
||||
<string name="integrations_disabled_info">During the Restore process, it is not possible to use payment integrations.</string>
|
||||
</resources>
|
|
@ -0,0 +1,16 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40dp"
|
||||
android:height="40dp"
|
||||
android:viewportWidth="40"
|
||||
android:viewportHeight="40">
|
||||
<path
|
||||
android:pathData="M0,20C0,8.954 8.954,0 20,0C31.046,0 40,8.954 40,20C40,31.046 31.046,40 20,40C8.954,40 0,31.046 0,20Z"
|
||||
android:fillColor="#454243"/>
|
||||
<path
|
||||
android:pathData="M24.167,26.667H24C22.6,26.667 21.9,26.667 21.365,26.394C20.895,26.154 20.512,25.772 20.272,25.302C20,24.767 20,24.067 20,22.667V17.333C20,15.933 20,15.233 20.272,14.698C20.512,14.228 20.895,13.845 21.365,13.606C21.9,13.333 22.6,13.333 24,13.333H24.167M24.167,26.667C24.167,27.587 24.913,28.333 25.833,28.333C26.754,28.333 27.5,27.587 27.5,26.667C27.5,25.746 26.754,25 25.833,25C24.913,25 24.167,25.746 24.167,26.667ZM24.167,13.333C24.167,14.254 24.913,15 25.833,15C26.754,15 27.5,14.254 27.5,13.333C27.5,12.413 26.754,11.667 25.833,11.667C24.913,11.667 24.167,12.413 24.167,13.333ZM15.833,20L24.167,20M15.833,20C15.833,20.92 15.087,21.667 14.167,21.667C13.246,21.667 12.5,20.92 12.5,20C12.5,19.079 13.246,18.333 14.167,18.333C15.087,18.333 15.833,19.079 15.833,20ZM24.167,20C24.167,20.92 24.913,21.667 25.833,21.667C26.754,21.667 27.5,20.92 27.5,20C27.5,19.079 26.754,18.333 25.833,18.333C24.913,18.333 24.167,19.079 24.167,20Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.66667"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#E8E8E8"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -0,0 +1,16 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40dp"
|
||||
android:height="40dp"
|
||||
android:viewportWidth="40"
|
||||
android:viewportHeight="40">
|
||||
<path
|
||||
android:pathData="M0,20C0,8.954 8.954,0 20,0C31.046,0 40,8.954 40,20C40,31.046 31.046,40 20,40C8.954,40 0,31.046 0,20Z"
|
||||
android:fillColor="#EBEBE6"/>
|
||||
<path
|
||||
android:pathData="M24.167,26.667H24C22.6,26.667 21.9,26.667 21.365,26.394C20.895,26.154 20.512,25.772 20.272,25.302C20,24.767 20,24.067 20,22.667V17.333C20,15.933 20,15.233 20.272,14.698C20.512,14.228 20.895,13.845 21.365,13.606C21.9,13.333 22.6,13.333 24,13.333H24.167M24.167,26.667C24.167,27.587 24.913,28.333 25.833,28.333C26.754,28.333 27.5,27.587 27.5,26.667C27.5,25.746 26.754,25 25.833,25C24.913,25 24.167,25.746 24.167,26.667ZM24.167,13.333C24.167,14.254 24.913,15 25.833,15C26.754,15 27.5,14.254 27.5,13.333C27.5,12.413 26.754,11.667 25.833,11.667C24.913,11.667 24.167,12.413 24.167,13.333ZM15.833,20L24.167,20M15.833,20C15.833,20.92 15.087,21.667 14.167,21.667C13.246,21.667 12.5,20.92 12.5,20C12.5,19.079 13.246,18.333 14.167,18.333C15.087,18.333 15.833,19.079 15.833,20ZM24.167,20C24.167,20.92 24.913,21.667 25.833,21.667C26.754,21.667 27.5,20.92 27.5,20C27.5,19.079 26.754,18.333 25.833,18.333C24.913,18.333 24.167,19.079 24.167,20Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.66667"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#231F20"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -1,6 +1,7 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="settings_title">Settings</string>
|
||||
<string name="settings_advanced_settings">Advanced Settings</string>
|
||||
<string name="settings_integrations">Integrations</string>
|
||||
<string name="settings_about_us">About Us</string>
|
||||
<string name="settings_feedback">Send Us Feedback</string>
|
||||
<string name="settings_version">Version <xliff:g example="1" id="version">%s</xliff:g></string>
|
||||
|
|
Loading…
Reference in New Issue