Exchange Rate redesign

This commit is contained in:
Milan Cerovsky 2024-08-14 09:06:12 +02:00
parent 77f1c163ad
commit c03c595b28
54 changed files with 1625 additions and 138 deletions

View File

@ -0,0 +1,27 @@
package co.electriccoin.zcash.preference.model.entry
import co.electriccoin.zcash.preference.api.PreferenceProvider
data class NullableBooleanPreferenceDefault(
override val key: PreferenceKey,
private val defaultValue: Boolean?
) : PreferenceDefault<Boolean?> {
@Suppress("SwallowedException")
override suspend fun getValue(preferenceProvider: PreferenceProvider) =
preferenceProvider.getString(key)?.let {
try {
it.toBooleanStrict()
} catch (e: IllegalArgumentException) {
// TODO [#32]: Log coercion failure instead of just silently returning default
// TODO [#32]: https://github.com/Electric-Coin-Company/zashi-android/issues/32
defaultValue
}
} ?: defaultValue
override suspend fun putValue(
preferenceProvider: PreferenceProvider,
newValue: Boolean?
) {
preferenceProvider.putString(key, newValue.toString())
}
}

View File

@ -11,6 +11,18 @@ data class ZashiColors(
val textLight: Color,
val textLightSupport: Color,
val surfacePrimary: Color,
val bgPrimary: Color,
val defaultFg: Color,
val textTertiary: Color,
val textPrimary: Color,
val strokeSecondary: Color,
val btnTertiaryBg: Color,
val btnTertiaryFg: Color,
val btnPrimaryBg: Color,
val btnPrimaryBgDisabled: Color,
val btnPrimaryFg: Color,
val btnPrimaryFgDisabled: Color,
val btnTextFg: Color,
)
internal val LightZashiColorPalette =
@ -18,6 +30,18 @@ internal val LightZashiColorPalette =
textLight = Color(0xFFFFFFFF),
textLightSupport = Color(0xFFD9D8CF),
surfacePrimary = Color(0xFF282622),
bgPrimary = Color(0xFFFFFFFF),
defaultFg = Color(0xFFD9D8CF),
textPrimary = Color(0xFF231F20),
textTertiary = Color(0xFF716C5D),
strokeSecondary = Color(0xFFEBEBE6),
btnTertiaryBg = Color(0xFFEBEBE6),
btnTertiaryFg = Color(0xFF4D4941),
btnPrimaryBg = Color(0xFF231F20),
btnPrimaryBgDisabled = Color(0xFFEBEBE6),
btnPrimaryFg = Color(0xFFFFFFFF),
btnPrimaryFgDisabled = Color(0xFF94907B),
btnTextFg = Color(0xFF231F20)
)
internal val DarkZashiColorPalette =
@ -25,6 +49,18 @@ internal val DarkZashiColorPalette =
textLight = Color(0xFFE8E8E8),
textLightSupport = Color(0xFFBDBBBC),
surfacePrimary = Color(0xFF454243),
bgPrimary = Color(0xFF231F20),
defaultFg = Color(0xFFBDBBBC),
textPrimary = Color(0xFFE8E8E8),
textTertiary = Color(0xFFBDBBBC),
strokeSecondary = Color(0xFF454243),
btnTertiaryBg = Color(0xFF343031),
btnTertiaryFg = Color(0xFFD2D1D2),
btnPrimaryBg = Color(0xFFFFFFFF),
btnPrimaryBgDisabled = Color(0xFF343031),
btnPrimaryFg = Color(0xFF231F20),
btnPrimaryFgDisabled = Color(0xFF7E7C7C),
btnTextFg = Color(0xFFE8E8E8)
)
@Suppress("CompositionLocalAllowlist")

View File

@ -56,6 +56,7 @@ android {
"src/main/res/ui/wallet_address",
"src/main/res/ui/warning",
"src/main/res/ui/whats_new",
"src/main/res/ui/exchange_rate",
)
)
}

View File

@ -1,6 +1,7 @@
package co.electriccoin.zcash.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
@ -27,6 +28,7 @@ import co.electriccoin.zcash.ui.NavigationTargets.ABOUT
import co.electriccoin.zcash.ui.NavigationTargets.ADVANCED_SETTINGS
import co.electriccoin.zcash.ui.NavigationTargets.CHOOSE_SERVER
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.NOT_ENOUGH_SPACE
@ -34,6 +36,7 @@ import co.electriccoin.zcash.ui.NavigationTargets.SCAN
import co.electriccoin.zcash.ui.NavigationTargets.SEED_RECOVERY
import co.electriccoin.zcash.ui.NavigationTargets.SEND_CONFIRMATION
import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS
import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS_EXCHANGE_RATE_OPT_IN
import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT
import co.electriccoin.zcash.ui.NavigationTargets.WHATS_NEW
import co.electriccoin.zcash.ui.common.compose.LocalNavController
@ -51,6 +54,8 @@ import co.electriccoin.zcash.ui.screen.authentication.WrapAuthentication
import co.electriccoin.zcash.ui.screen.chooseserver.WrapChooseServer
import co.electriccoin.zcash.ui.screen.deletewallet.WrapDeleteWallet
import co.electriccoin.zcash.ui.screen.disconnected.WrapDisconnected
import co.electriccoin.zcash.ui.screen.exchangerate.optin.AndroidExchangeRateOptIn
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.scan.WrapScanValidator
@ -86,6 +91,18 @@ internal fun MainActivity.Navigation() {
val (deleteWalletAuthentication, setDeleteWalletAuthentication) =
rememberSaveable { mutableStateOf(false) }
LaunchedEffect(Unit) {
walletViewModel.navigationCommand.collect {
navController.navigateJustOnce(it)
}
}
LaunchedEffect(Unit) {
walletViewModel.backNavigationCommand.collect {
navController.popBackStack()
}
}
NavHost(
navController = navController,
startDestination = HOME,
@ -145,6 +162,9 @@ internal fun MainActivity.Navigation() {
unProtectedDestination = DELETE_WALLET
)
},
onCurrencyConversion = {
navController.navigateJustOnce(SETTINGS_EXCHANGE_RATE_OPT_IN)
}
)
when {
@ -214,6 +234,12 @@ internal fun MainActivity.Navigation() {
composable(WHATS_NEW) {
WrapWhatsNew()
}
composable(EXCHANGE_RATE_OPT_IN) {
AndroidExchangeRateOptIn()
}
composable(SETTINGS_EXCHANGE_RATE_OPT_IN) {
AndroidSettingsExchangeRateOptIn()
}
composable(SCAN) {
WrapScanValidator(
onScanValid = { scanResult ->
@ -445,4 +471,6 @@ object NavigationTargets {
const val SETTINGS = "settings"
const val SUPPORT = "support"
const val WHATS_NEW = "whats_new"
const val SETTINGS_EXCHANGE_RATE_OPT_IN = "settings_exchange_rate_opt_in"
const val EXCHANGE_RATE_OPT_IN = "EXCHANGE_RATE_OPT_IN"
}

View File

@ -22,7 +22,6 @@ import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.sdk.extension.toZecStringFull
import cash.z.ecc.sdk.type.ZcashCurrency
import co.electriccoin.zcash.ui.StyledExchangeBalance
import co.electriccoin.zcash.ui.common.extension.asZecAmountTriple
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
import co.electriccoin.zcash.ui.design.R
@ -35,6 +34,7 @@ import co.electriccoin.zcash.ui.design.component.StyledBalanceDefaults
import co.electriccoin.zcash.ui.design.component.ZecAmountTriple
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.ObserveFiatCurrencyResultFixture
import co.electriccoin.zcash.ui.screen.exchangerate.widget.StyledExchangeBalance
@Preview(device = Devices.PIXEL_2)
@Composable
@ -151,7 +151,9 @@ fun BalanceWidget(
parts = balanceState.totalBalance.toZecStringFull().asZecAmountTriple()
)
Spacer(modifier = Modifier.height(16.dp))
if (balanceState.exchangeRate is ExchangeRateState.Data) {
Spacer(modifier = Modifier.height(16.dp))
}
StyledExchangeBalance(
zatoshi = balanceState.totalBalance,
@ -159,7 +161,9 @@ fun BalanceWidget(
isHideBalances = isHideBalances
)
Spacer(modifier = Modifier.height(12.dp))
if (balanceState.exchangeRate is ExchangeRateState.Data) {
Spacer(modifier = Modifier.height(12.dp))
}
Row(verticalAlignment = Alignment.CenterVertically) {
if (isReferenceToBalances) {

View File

@ -26,8 +26,10 @@ import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.sdk.type.fromResources
import co.electriccoin.zcash.global.getInstance
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.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.common.compose.BalanceState
import co.electriccoin.zcash.ui.common.extension.throttle
@ -55,6 +57,7 @@ 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
@ -78,7 +81,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
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
@ -97,6 +99,10 @@ class WalletViewModel(
*/
private val persistWalletMutex = Mutex()
val navigationCommand = MutableSharedFlow<String>()
val backNavigationCommand = MutableSharedFlow<Unit>()
/**
* Synchronizer that is retained long enough to survive configuration changes.
*/
@ -292,18 +298,25 @@ class WalletViewModel(
initialValue = TransactionHistorySyncState.Loading
)
val isExchangeRateUsdOptedIn = nullableBooleanStateFlow(StandardPreferenceKeys.EXCHANGE_RATE_USD_OPTED_IN)
@OptIn(ExperimentalCoroutinesApi::class)
private val exchangeRateUsdInternal =
synchronizer
.filterNotNull()
.flatMapLatest { synchronizer ->
synchronizer.exchangeRateUsd
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()
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(USD_EXCHANGE_REFRESH_LOCK_THRESHOLD),
initialValue = ObserveFiatCurrencyResult(isLoading = false, currencyConversion = null)
)
private val usdExchangeRateTimestamp =
exchangeRateUsdInternal
@ -312,23 +325,56 @@ class WalletViewModel(
}
.distinctUntilChanged()
private var lastExchangeRateUsdValue: ExchangeRateState = ExchangeRateState.OptedOut
val exchangeRateUsd: StateFlow<ExchangeRateState> =
channelFlow {
var lastValue = ExchangeRateState(onRefresh = ::refreshExchangeRateUsd)
combine(
isExchangeRateUsdOptedIn,
exchangeRateUsdInternal,
staleExchangeRateUsdLock.state,
refreshExchangeRateUsdLock.state,
) { exchangeRate, isStale, isRefreshEnabled ->
lastValue =
lastValue.copy(
isLoading = exchangeRate.isLoading,
isStale = isStale,
isRefreshEnabled = isRefreshEnabled,
currencyConversion = exchangeRate.currencyConversion,
)
lastValue
) { 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" }
@ -342,7 +388,7 @@ class WalletViewModel(
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = ExchangeRateState(onRefresh = ::refreshExchangeRateUsd)
initialValue = ExchangeRateState.OptedOut
)
/**
@ -380,7 +426,7 @@ class WalletViewModel(
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
BalanceState.None(ExchangeRateState(onRefresh = ::refreshExchangeRateUsd))
BalanceState.None(ExchangeRateState.OptedOut)
)
private val refreshExchangeRateUsdLock =
@ -400,11 +446,32 @@ class WalletViewModel(
viewModelScope.launch {
val synchronizer = synchronizer.filterNotNull().first()
val value = exchangeRateUsd.value
if (value.isRefreshEnabled && !value.isLoading) {
if (value is ExchangeRateState.Data && value.isRefreshEnabled && !value.isLoading) {
synchronizer.refreshExchangeRateUsd()
}
}
fun optInExchangeRateUsd(optIn: Boolean) =
viewModelScope.launch {
setNullableBooleanPreference(StandardPreferenceKeys.EXCHANGE_RATE_USD_OPTED_IN, optIn)
backNavigationCommand.emit(Unit)
}
fun dismissOptInExchangeRateUsd() =
viewModelScope.launch {
setNullableBooleanPreference(StandardPreferenceKeys.EXCHANGE_RATE_USD_OPTED_IN, false)
backNavigationCommand.emit(Unit)
}
private fun dismissWidgetOptInExchangeRateUsd() {
setNullableBooleanPreference(StandardPreferenceKeys.EXCHANGE_RATE_USD_OPTED_IN, false)
}
private fun showOptInExchangeRateUsd() =
viewModelScope.launch {
navigationCommand.emit(EXCHANGE_RATE_OPT_IN)
}
/**
* Creates a wallet asynchronously and then persists it. Clients observe
* [secretState] to see the side effects. This would be used for a user creating a new wallet.
@ -579,6 +646,26 @@ class WalletViewModel(
// Nothing to close
}
}
private fun nullableBooleanStateFlow(default: NullableBooleanPreferenceDefault): StateFlow<Boolean?> =
flow {
val preferenceProvider = StandardPreferenceSingleton.getInstance(getApplication())
emitAll(default.observe(preferenceProvider))
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
null
)
private fun setNullableBooleanPreference(
default: NullableBooleanPreferenceDefault,
newState: Boolean
) {
viewModelScope.launch {
val prefs = StandardPreferenceSingleton.getInstance(getApplication())
default.putValue(prefs, newState)
}
}
}
/**
@ -718,5 +805,5 @@ 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
private val USD_EXCHANGE_REFRESH_LOCK_THRESHOLD = 10.seconds
private val USD_EXCHANGE_STALE_LOCK_THRESHOLD = 20.seconds

View File

@ -3,13 +3,20 @@ package co.electriccoin.zcash.ui.common.wallet
import cash.z.ecc.android.sdk.model.FiatCurrency
import cash.z.ecc.android.sdk.model.FiatCurrencyConversion
data class ExchangeRateState(
val isLoading: Boolean = true,
val isStale: Boolean = false,
val isRefreshEnabled: Boolean = true,
val currencyConversion: FiatCurrencyConversion? = null,
val onRefresh: () -> Unit
) {
val fiatCurrency: FiatCurrency
get() = FiatCurrency.USD
sealed interface ExchangeRateState {
data class Data(
val isLoading: Boolean = true,
val isStale: Boolean = false,
val isRefreshEnabled: Boolean = true,
val currencyConversion: FiatCurrencyConversion? = null,
val fiatCurrency: FiatCurrency = FiatCurrency.USD,
val onRefresh: () -> Unit,
) : ExchangeRateState
data class OptIn(
val onDismissClick: () -> Unit = {},
val onPrimaryClick: () -> Unit = {}
) : ExchangeRateState
data object OptedOut : ExchangeRateState
}

View File

@ -3,5 +3,5 @@ package co.electriccoin.zcash.ui.fixture
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
object ExchangeRateStateFixture {
fun new() = ExchangeRateState {}
fun new() = ExchangeRateState.OptedOut
}

View File

@ -14,5 +14,5 @@ object ObserveFiatCurrencyResultFixture {
timestamp = Clock.System.now(),
priceOfZec = 25.0
),
) = ExchangeRateState(isLoading, isStale, isRefreshEnabled, currencyConversion) {}
) = ExchangeRateState.Data(isLoading, isStale, isRefreshEnabled, currencyConversion) {}
}

View File

@ -2,6 +2,7 @@ package co.electriccoin.zcash.ui.preference
import co.electriccoin.zcash.preference.model.entry.BooleanPreferenceDefault
import co.electriccoin.zcash.preference.model.entry.IntegerPreferenceDefault
import co.electriccoin.zcash.preference.model.entry.NullableBooleanPreferenceDefault
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
import co.electriccoin.zcash.ui.common.model.OnboardingState
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
@ -68,4 +69,9 @@ object StandardPreferenceKeys {
PreferenceKey("IS_HIDE_BALANCES"),
false
)
val EXCHANGE_RATE_USD_OPTED_IN =
NullableBooleanPreferenceDefault(
PreferenceKey("EXCHANGE_RATE_USD_OPTED_IN"),
null
)
}

View File

@ -1,6 +1,14 @@
package co.electriccoin.zcash.ui.screen.account.view
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -10,6 +18,11 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@ -17,7 +30,7 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.model.FiatCurrencyConversion
import cash.z.ecc.android.sdk.model.Zatoshi
import co.electriccoin.zcash.ui.R
@ -39,14 +52,22 @@ import co.electriccoin.zcash.ui.screen.account.AccountTag
import co.electriccoin.zcash.ui.screen.account.fixture.TransactionsFixture
import co.electriccoin.zcash.ui.screen.account.model.TransactionUiState
import co.electriccoin.zcash.ui.screen.balances.model.StatusAction
import co.electriccoin.zcash.ui.screen.exchangerate.widget.StyledExchangeOptIn
import co.electriccoin.zcash.ui.util.PreviewScreens
import kotlinx.coroutines.delay
import kotlinx.datetime.Clock
import kotlin.time.Duration.Companion.seconds
@Preview("Account No History")
@Suppress("UnusedPrivateMember")
@PreviewScreens
@Composable
private fun HistoryLoadingComposablePreview() {
ZcashTheme(forceDarkMode = false) {
Account(
balanceState = BalanceStateFixture.new(),
balanceState =
BalanceStateFixture.new(
exchangeRate = ExchangeRateState.OptIn(onDismissClick = {})
),
isHideBalances = false,
goBalances = {},
goSettings = {},
@ -64,10 +85,11 @@ private fun HistoryLoadingComposablePreview() {
}
}
@Suppress("UnusedPrivateMember")
@Composable
@Preview("Account History List")
@PreviewScreens
private fun HistoryListComposablePreview() {
ZcashTheme(forceDarkMode = false) {
ZcashTheme {
@Suppress("MagicNumber")
Account(
balanceState =
@ -75,7 +97,7 @@ private fun HistoryListComposablePreview() {
totalBalance = Zatoshi(value = 123_000_000L),
spendableBalance = Zatoshi(value = 123_000_000L),
exchangeRate =
ExchangeRateState(
ExchangeRateState.Data(
isLoading = false,
isRefreshEnabled = true,
currencyConversion =
@ -147,7 +169,8 @@ internal fun Account(
top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault,
// We intentionally do not set the bottom and horizontal paddings here. Those are set by the
// underlying transaction history composable
)
),
paddingValues = paddingValues
)
// Show synchronization status popup
@ -205,7 +228,7 @@ private fun AccountTopAppBar(
}
@Composable
@Suppress("LongParameterList")
@Suppress("LongParameterList", "ModifierNotUsedAtRoot")
private fun AccountMainContent(
balanceState: BalanceState,
goBalances: () -> Unit,
@ -216,32 +239,72 @@ private fun AccountMainContent(
transactionState: TransactionUiState,
walletSnapshot: WalletSnapshot,
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues()
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
var delayedExchangeRateState by remember { mutableStateOf<ExchangeRateState?>(null) }
BalancesStatus(
balanceState = balanceState,
goBalances = goBalances,
isHideBalances = isHideBalances,
modifier =
Modifier
.padding(horizontal = ZcashTheme.dimens.screenHorizontalSpacingRegular),
)
LaunchedEffect(key1 = balanceState.exchangeRate) {
if (delayedExchangeRateState == null && balanceState.exchangeRate is ExchangeRateState.OptIn) {
delay(1.seconds)
}
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
delayedExchangeRateState = balanceState.exchangeRate
}
HistoryContainer(
isHideBalances = isHideBalances,
onStatusClick = onStatusClick,
onTransactionItemAction = onTransactionItemAction,
transactionState = transactionState,
walletRestoringState = isWalletRestoringState,
walletSnapshot = walletSnapshot,
)
Box {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
val bottomPadding =
animateDpAsState(
targetValue = if (delayedExchangeRateState is ExchangeRateState.OptIn) 76.dp else 0.dp,
label = "bottom padding animation"
)
BalancesStatus(
balanceState = balanceState,
goBalances = goBalances,
isHideBalances = isHideBalances,
modifier =
Modifier
.padding(
start = ZcashTheme.dimens.screenHorizontalSpacingRegular,
end = ZcashTheme.dimens.screenHorizontalSpacingRegular,
bottom = bottomPadding.value
),
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
HistoryContainer(
isHideBalances = isHideBalances,
onStatusClick = onStatusClick,
onTransactionItemAction = onTransactionItemAction,
transactionState = transactionState,
walletRestoringState = isWalletRestoringState,
walletSnapshot = walletSnapshot,
)
}
AnimatedVisibility(
visible = delayedExchangeRateState is ExchangeRateState.OptIn,
enter = fadeIn() + slideInVertically(),
exit = fadeOut() + slideOutVertically(),
) {
Column {
Spacer(modifier = Modifier.height(80.dp + paddingValues.calculateTopPadding()))
StyledExchangeOptIn(
modifier = Modifier.padding(horizontal = 24.dp),
state =
(delayedExchangeRateState as? ExchangeRateState.OptIn) ?: ExchangeRateState.OptIn(
onDismissClick = {},
)
)
}
}
}
}

View File

@ -11,6 +11,7 @@ import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.advancedsettings.view.AdvancedSettings
@Suppress("LongParameterList")
@Composable
internal fun MainActivity.WrapAdvancedSettings(
goBack: () -> Unit,
@ -18,6 +19,7 @@ internal fun MainActivity.WrapAdvancedSettings(
goExportPrivateData: () -> Unit,
goChooseServer: () -> Unit,
goSeedRecovery: () -> Unit,
onCurrencyConversion: () -> Unit
) {
val walletViewModel by viewModels<WalletViewModel>()
@ -29,7 +31,8 @@ internal fun MainActivity.WrapAdvancedSettings(
goExportPrivateData = goExportPrivateData,
goChooseServer = goChooseServer,
goSeedRecovery = goSeedRecovery,
topAppBarSubTitleState = walletState
topAppBarSubTitleState = walletState,
onCurrencyConversion = onCurrencyConversion
)
}
@ -41,6 +44,7 @@ private fun WrapAdvancedSettings(
goChooseServer: () -> Unit,
goSeedRecovery: () -> Unit,
goDeleteWallet: () -> Unit,
onCurrencyConversion: () -> Unit,
topAppBarSubTitleState: TopAppBarSubTitleState,
) {
BackHandler {
@ -54,5 +58,6 @@ private fun WrapAdvancedSettings(
onChooseServer = goChooseServer,
onSeedRecovery = goSeedRecovery,
topAppBarSubTitleState = topAppBarSubTitleState,
onCurrencyConversion = onCurrencyConversion
)
}

View File

@ -41,7 +41,8 @@ private fun PreviewAdvancedSettings() {
onExportPrivateData = {},
onChooseServer = {},
onSeedRecovery = {},
topAppBarSubTitleState = TopAppBarSubTitleState.None
topAppBarSubTitleState = TopAppBarSubTitleState.None,
onCurrencyConversion = {}
)
}
}
@ -54,6 +55,7 @@ fun AdvancedSettings(
onExportPrivateData: () -> Unit,
onChooseServer: () -> Unit,
onSeedRecovery: () -> Unit,
onCurrencyConversion: () -> Unit,
topAppBarSubTitleState: TopAppBarSubTitleState,
) {
BlankBgScaffold(
@ -80,6 +82,7 @@ fun AdvancedSettings(
onExportPrivateData = onExportPrivateData,
onSeedRecovery = onSeedRecovery,
onChooseServer = onChooseServer,
onCurrencyConversion = onCurrencyConversion
)
}
}
@ -108,11 +111,13 @@ private fun AdvancedSettingsTopAppBar(
)
}
@Suppress("LongParameterList")
@Composable
private fun AdvancedSettingsMainContent(
onDeleteWallet: () -> Unit,
onExportPrivateData: () -> Unit,
onChooseServer: () -> Unit,
onCurrencyConversion: () -> Unit,
onSeedRecovery: () -> Unit,
modifier: Modifier = Modifier,
) {
@ -144,6 +149,14 @@ private fun AdvancedSettingsMainContent(
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(dimens.spacingDefault))
PrimaryButton(
onClick = onCurrencyConversion,
text = stringResource(R.string.advanced_settings_currency_conversion),
modifier = Modifier.fillMaxWidth()
)
Spacer(
modifier =
Modifier

View File

@ -0,0 +1,118 @@
package co.electriccoin.zcash.ui.screen.exchangerate
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
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.design.theme.ZcashTheme
@Suppress("LongMethod")
@Composable
internal fun BaseExchangeRateOptIn(
onDismiss: () -> Unit,
content: @Composable ColumnScope.() -> Unit,
footer: @Composable ColumnScope.() -> Unit,
) {
Scaffold {
Column(
modifier =
Modifier
.fillMaxSize()
.padding(
start = 24.dp,
top = it.calculateTopPadding() + 12.dp,
end = 24.dp,
bottom = it.calculateBottomPadding() + 24.dp
)
) {
Button(
contentPadding = PaddingValues(0.dp),
modifier = Modifier.size(40.dp),
onClick = onDismiss,
shape = RoundedCornerShape(12.dp),
colors =
ButtonDefaults.buttonColors(
containerColor = ZcashTheme.zashiColors.btnPrimaryBgDisabled
)
) {
Image(
painter = painterResource(id = R.drawable.ic_exchange_rate_close),
contentDescription = "",
colorFilter = ColorFilter.tint(ZcashTheme.zashiColors.btnTertiaryFg)
)
}
Spacer(modifier = Modifier.height(28.dp))
Column(
modifier =
Modifier
.weight(1f)
.verticalScroll(rememberScrollState())
) {
Image(painter = painterResource(Image), contentDescription = "")
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Currency Conversion",
color = ZcashTheme.zashiColors.textPrimary,
fontSize = 24.sp,
style = ZcashTheme.extendedTypography.restoringTopAppBarStyle,
)
content()
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.weight(1f))
Row {
Image(
painter = painterResource(R.drawable.ic_exchange_rate_info),
contentDescription = "",
colorFilter = ColorFilter.tint(ZcashTheme.zashiColors.textPrimary)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(R.string.exchange_rate_opt_in_note),
color = ZcashTheme.zashiColors.textTertiary,
fontSize = 12.sp
)
}
}
Spacer(modifier = Modifier.height(20.dp))
footer()
}
}
}
private val Image: Int
@DrawableRes
@Composable
get() =
if (isSystemInDarkTheme()) {
R.drawable.exchange_rate
} else {
R.drawable.exchange_rate_light
}

View File

@ -0,0 +1,29 @@
package co.electriccoin.zcash.ui.screen.exchangerate
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@Composable
internal fun SecondaryCard(
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit,
) {
Card(
modifier = modifier,
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.elevatedCardElevation(0.dp),
border = BorderStroke(1.dp, ZcashTheme.zashiColors.strokeSecondary),
colors =
CardDefaults.cardColors(
containerColor = ZcashTheme.zashiColors.bgPrimary
),
content = content
)
}

View File

@ -0,0 +1,60 @@
package co.electriccoin.zcash.ui.screen.exchangerate
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@Composable
internal fun ZashiButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
colors: ButtonColors = ZashiButtonDefaults.primaryButtonColors(),
content: @Composable RowScope.() -> Unit
) {
Button(
onClick = onClick,
modifier = modifier,
shape = RoundedCornerShape(12.dp),
enabled = enabled,
colors = colors,
content = content
)
}
object ZashiButtonDefaults {
@Composable
fun primaryButtonColors(
containerColor: Color = ZcashTheme.zashiColors.btnPrimaryBg,
contentColor: Color = ZcashTheme.zashiColors.btnPrimaryFg,
disabledContainerColor: Color = ZcashTheme.zashiColors.btnPrimaryBgDisabled,
disabledContentColor: Color = ZcashTheme.zashiColors.btnPrimaryFgDisabled,
): ButtonColors =
ButtonDefaults.buttonColors(
containerColor = containerColor,
contentColor = contentColor,
disabledContainerColor = disabledContainerColor,
disabledContentColor = disabledContentColor
)
@Composable
fun tertiaryButtonColors(
containerColor: Color = ZcashTheme.zashiColors.btnTertiaryBg,
contentColor: Color = ZcashTheme.zashiColors.btnTertiaryFg,
disabledContainerColor: Color = Color.Unspecified,
disabledContentColor: Color = Color.Unspecified,
): ButtonColors =
ButtonDefaults.buttonColors(
containerColor = containerColor,
contentColor = contentColor,
disabledContainerColor = disabledContainerColor,
disabledContentColor = disabledContentColor
)
}

View File

@ -0,0 +1,46 @@
package co.electriccoin.zcash.ui.screen.exchangerate
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@Composable
internal fun ZashiTextButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
colors: ButtonColors = ZashiTextButtonDefaults.textButtonColors(),
content: @Composable RowScope.() -> Unit
) {
TextButton(
onClick = onClick,
modifier = modifier,
shape = RoundedCornerShape(12.dp),
enabled = enabled,
colors = colors,
content = content
)
}
object ZashiTextButtonDefaults {
@Composable
fun textButtonColors(
containerColor: Color = Color.Unspecified,
contentColor: Color = ZcashTheme.zashiColors.btnTextFg,
disabledContainerColor: Color = Color.Unspecified,
disabledContentColor: Color = Color.Unspecified,
): ButtonColors =
ButtonDefaults.textButtonColors(
containerColor = containerColor,
contentColor = contentColor,
disabledContainerColor = disabledContainerColor,
disabledContentColor = disabledContentColor
)
}

View File

@ -0,0 +1,22 @@
package co.electriccoin.zcash.ui.screen.exchangerate.optin
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import co.electriccoin.zcash.ui.common.compose.LocalActivity
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
@Composable
fun AndroidExchangeRateOptIn() {
val activity = LocalActivity.current
val walletViewModel by activity.viewModels<WalletViewModel>()
BackHandler {
walletViewModel.dismissOptInExchangeRateUsd()
}
ExchangeRateOptIn(
onEnabledClick = { walletViewModel.optInExchangeRateUsd(true) },
onDismiss = { walletViewModel.dismissOptInExchangeRateUsd() }
)
}

View File

@ -0,0 +1,118 @@
package co.electriccoin.zcash.ui.screen.exchangerate.optin
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.design.component.BlankSurface
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.exchangerate.BaseExchangeRateOptIn
import co.electriccoin.zcash.ui.screen.exchangerate.ZashiButton
import co.electriccoin.zcash.ui.screen.exchangerate.ZashiButtonDefaults
import co.electriccoin.zcash.ui.screen.exchangerate.ZashiTextButton
import co.electriccoin.zcash.ui.util.PreviewScreens
@Composable
fun ExchangeRateOptIn(
onEnabledClick: () -> Unit,
onDismiss: () -> Unit,
) {
BaseExchangeRateOptIn(
onDismiss = onDismiss,
content = {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.exchange_rate_opt_in_description),
color = ZcashTheme.zashiColors.textTertiary,
fontSize = 14.sp,
)
Spacer(modifier = Modifier.height(24.dp))
InfoItem(
modifier = Modifier,
image = R.drawable.ic_exchange_rate_info_1,
title = stringResource(R.string.exchange_rate_info_title_1),
subtitle = stringResource(R.string.exchange_rate_info_subtitle_1),
)
Spacer(modifier = Modifier.height(20.dp))
InfoItem(
modifier = Modifier,
image = R.drawable.ic_exchange_rate_info_2,
title = stringResource(R.string.exchange_rate_info_title_2),
subtitle = stringResource(R.string.exchange_rate_info_subtitle_2),
)
},
footer = {
ZashiButton(
modifier = Modifier.fillMaxWidth(),
onClick = onEnabledClick,
colors = ZashiButtonDefaults.primaryButtonColors()
) {
Text(
text = stringResource(R.string.exchange_rate_opt_in_enable)
)
}
ZashiTextButton(
modifier = Modifier.fillMaxWidth(),
onClick = onDismiss,
) {
Text(text = stringResource(R.string.exchange_rate_opt_in_skip))
}
}
)
}
@Composable
private fun InfoItem(
@DrawableRes image: Int,
title: String,
subtitle: String,
modifier: Modifier = Modifier,
) {
Row(modifier) {
Image(
painter = painterResource(image),
contentDescription = ""
)
Spacer(modifier = Modifier.width(12.dp))
Column(
verticalArrangement = Arrangement.Center
) {
Text(
text = title,
color = ZcashTheme.zashiColors.textPrimary,
fontSize = 16.sp,
style = ZcashTheme.extendedTypography.restoringTopAppBarStyle,
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = subtitle,
color = ZcashTheme.zashiColors.textTertiary,
fontSize = 14.sp,
)
}
Spacer(modifier = Modifier.weight(1f))
}
}
@Suppress("UnusedPrivateMember")
@PreviewScreens
@Composable
private fun CurrencyConversionOptInPreview() =
ZcashTheme {
BlankSurface {
ExchangeRateOptIn(onEnabledClick = {}, onDismiss = {})
}
}

View File

@ -0,0 +1,27 @@
package co.electriccoin.zcash.ui.screen.exchangerate.settings
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.common.compose.LocalActivity
import co.electriccoin.zcash.ui.common.compose.LocalNavController
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
@Composable
fun AndroidSettingsExchangeRateOptIn() {
val activity = LocalActivity.current
val navController = LocalNavController.current
val walletViewModel by activity.viewModels<WalletViewModel>()
val isOptedIn = walletViewModel.isExchangeRateUsdOptedIn.collectAsStateWithLifecycle().value ?: false
BackHandler {
navController.popBackStack()
}
SettingsExchangeRateOptIn(
isOptedIn = isOptedIn,
onSaveClick = { walletViewModel.optInExchangeRateUsd(it) },
onDismiss = { navController.popBackStack() }
)
}

View File

@ -0,0 +1,191 @@
package co.electriccoin.zcash.ui.screen.exchangerate.settings
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
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.design.component.BlankSurface
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.exchangerate.BaseExchangeRateOptIn
import co.electriccoin.zcash.ui.screen.exchangerate.SecondaryCard
import co.electriccoin.zcash.ui.screen.exchangerate.ZashiButton
import co.electriccoin.zcash.ui.screen.exchangerate.ZashiButtonDefaults
import co.electriccoin.zcash.ui.util.PreviewScreens
@Composable
fun SettingsExchangeRateOptIn(
isOptedIn: Boolean,
onDismiss: () -> Unit,
onSaveClick: (Boolean) -> Unit
) {
var isOptInSelected by remember(isOptedIn) { mutableStateOf(isOptedIn) }
val isButtonDisabled by remember {
derivedStateOf {
(isOptedIn && isOptInSelected) || (!isOptedIn && !isOptInSelected)
}
}
BaseExchangeRateOptIn(
onDismiss = onDismiss,
content = {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.exchange_rate_opt_in_description_settings),
color = ZcashTheme.zashiColors.textTertiary,
fontSize = 14.sp,
)
Spacer(modifier = Modifier.height(24.dp))
Option(
modifier = Modifier.fillMaxWidth(),
image = OptIn,
selectionImage = if (isOptInSelected) Checked else Unchecked,
title = stringResource(R.string.exchange_rate_opt_in_option_title),
subtitle = stringResource(R.string.exchange_rate_opt_in_option_subtitle),
onClick = { isOptInSelected = true }
)
Spacer(modifier = Modifier.height(12.dp))
Option(
modifier = Modifier.fillMaxWidth(),
image = OptOut,
selectionImage = if (!isOptInSelected) Checked else Unchecked,
title = stringResource(R.string.exchange_rate_opt_out_option_title),
subtitle = stringResource(R.string.exchange_rate_opt_out_option_subtitle),
onClick = { isOptInSelected = false }
)
},
footer = {
ZashiButton(
modifier = Modifier.fillMaxWidth(),
onClick = { onSaveClick(isOptInSelected) },
enabled = !isButtonDisabled,
colors = ZashiButtonDefaults.primaryButtonColors()
) {
Text(text = stringResource(R.string.exchange_rate_opt_in_save))
}
}
)
}
@Suppress("LongParameterList")
@Composable
private fun Option(
@DrawableRes image: Int,
@DrawableRes selectionImage: Int,
title: String,
subtitle: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
SecondaryCard(
modifier =
modifier.clickable(
onClick = onClick,
indication = null,
interactionSource = remember { MutableInteractionSource() },
)
) {
Row(
Modifier.padding(20.dp)
) {
Image(
painter = painterResource(image),
contentDescription = ""
)
Spacer(modifier = Modifier.width(12.dp))
Column(
verticalArrangement = Arrangement.Center
) {
Text(
text = title,
color = ZcashTheme.zashiColors.textPrimary,
fontSize = 16.sp,
style = ZcashTheme.extendedTypography.restoringTopAppBarStyle,
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = subtitle,
color = ZcashTheme.zashiColors.textTertiary,
fontSize = 14.sp,
)
}
Spacer(modifier = Modifier.weight(1f))
Image(
painter = painterResource(selectionImage),
contentDescription = ""
)
}
}
}
private val OptIn: Int
@DrawableRes
@Composable
get() =
if (isSystemInDarkTheme()) {
R.drawable.ic_opt_in
} else {
R.drawable.ic_opt_in_light
}
private val OptOut: Int
@DrawableRes
@Composable
get() =
if (isSystemInDarkTheme()) {
R.drawable.ic_opt_out
} else {
R.drawable.ic_opt_out_light
}
private val Checked: Int
@DrawableRes
@Composable
get() =
if (isSystemInDarkTheme()) {
R.drawable.ic_checkbox_checked
} else {
R.drawable.ic_checkbox_checked_light
}
private val Unchecked: Int
@DrawableRes
@Composable
get() =
if (isSystemInDarkTheme()) {
R.drawable.ic_checkbox_unchecked
} else {
R.drawable.ic_checkbox_unchecked_light
}
@Suppress("UnusedPrivateMember")
@PreviewScreens
@Composable
private fun SettingsExchangeRateOptInPreview() =
ZcashTheme {
BlankSurface {
SettingsExchangeRateOptIn(isOptedIn = true, onDismiss = {}, onSaveClick = {})
}
}

View File

@ -1,6 +1,6 @@
@file:Suppress("TooManyFunctions")
package co.electriccoin.zcash.ui
package co.electriccoin.zcash.ui.screen.exchangerate.widget
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.MutableTransitionState
@ -39,6 +39,7 @@ import cash.z.ecc.android.sdk.model.FiatCurrencyConversion
import cash.z.ecc.android.sdk.model.MonetarySeparators
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.toFiatString
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.extension.toKotlinLocale
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
import co.electriccoin.zcash.ui.design.component.BlankSurface
@ -67,56 +68,47 @@ fun StyledExchangeBalance(
textColor: Color = ZcashTheme.exchangeRateColors.btnSecondaryFg,
style: TextStyle = ZcashTheme.typography.primary.titleSmall.copy(fontWeight = FontWeight.SemiBold)
) {
if ((state.isStale && !state.isLoading) ||
(!state.isLoading && state.currencyConversion == null)
) {
ExchangeRateUnavailableButton(
textColor = textColor,
style = style,
modifier = modifier
)
} else {
ExchangeAvailableRateButton(
style = style,
textColor = textColor,
zatoshi = zatoshi,
isHideBalances = isHideBalances,
state = state,
hiddenBalancePlaceholder = hiddenBalancePlaceholder
)
when (state) {
is ExchangeRateState.Data ->
if ((state.isStale && !state.isLoading) || (!state.isLoading && state.currencyConversion == null)) {
ExchangeRateUnavailableButton(
textColor = textColor,
style = style,
modifier = modifier
)
} else {
ExchangeAvailableRateLabelInternal(
style = style,
textColor = textColor,
zatoshi = zatoshi,
isHideBalances = isHideBalances,
state = state,
hiddenBalancePlaceholder = hiddenBalancePlaceholder
)
}
is ExchangeRateState.OptIn -> {
// do not show anything
}
ExchangeRateState.OptedOut -> {
// do not show anything
}
}
}
@Suppress("LongParameterList", "LongMethod")
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ExchangeAvailableRateButton(
private fun ExchangeAvailableRateLabelInternal(
style: TextStyle,
textColor: Color,
zatoshi: Zatoshi,
isHideBalances: Boolean,
state: ExchangeRateState,
state: ExchangeRateState.Data,
hiddenBalancePlaceholder: StringResource,
modifier: Modifier = Modifier,
) {
val currencySymbol = state.fiatCurrency.symbol
val text =
if (isHideBalances) {
"${currencySymbol}${hiddenBalancePlaceholder.getValue()}"
} else if (state.currencyConversion != null) {
val value =
zatoshi.toFiatString(
currencyConversion = state.currencyConversion,
locale = Locale.current.toKotlinLocale(),
monetarySeparators = MonetarySeparators.current(java.util.Locale.getDefault()),
includeSymbols = false
)
"$currencySymbol$value"
} else {
currencySymbol
}
val isEnabled = !state.isLoading && state.isRefreshEnabled
ExchangeRateButton(
@ -129,7 +121,7 @@ private fun ExchangeAvailableRateButton(
textColor = textColor,
) {
Text(
text = text,
text = createExchangeRateText(state, isHideBalances, hiddenBalancePlaceholder, zatoshi),
style = style,
maxLines = 1,
color = textColor
@ -177,6 +169,33 @@ private fun ExchangeAvailableRateButton(
}
}
@Composable
internal fun createExchangeRateText(
state: ExchangeRateState.Data,
isHideBalances: Boolean,
hiddenBalancePlaceholder: StringResource,
zatoshi: Zatoshi
): String {
val currencySymbol = state.fiatCurrency.symbol
val text =
if (isHideBalances) {
"${currencySymbol}${hiddenBalancePlaceholder.getValue()}"
} else if (state.currencyConversion != null) {
val value =
zatoshi.toFiatString(
currencyConversion = state.currencyConversion,
locale = Locale.current.toKotlinLocale(),
monetarySeparators = MonetarySeparators.current(java.util.Locale.getDefault()),
includeSymbols = false
)
"$currencySymbol$value"
} else {
currencySymbol
}
return text
}
@Composable
private fun ExchangeRateUnavailableButton(
textColor: Color,
@ -199,7 +218,7 @@ private fun ExchangeRateUnavailableButton(
textColor = textColor,
) {
Text(
text = stringResource(id = R.string.balances_exchange_rate_unavailable),
text = stringResource(id = R.string.exchange_rate_unavailable_title),
style = style,
maxLines = 1,
color = textColor
@ -211,7 +230,7 @@ private fun ExchangeRateUnavailableButton(
modifier =
Modifier
.align(CenterVertically),
painter = painterResource(R.drawable.ic_unavailable_exchange_rate),
painter = painterResource(R.drawable.ic_exchange_rate_info),
contentDescription = "",
colorFilter = ColorFilter.tint(textColor)
)
@ -219,7 +238,7 @@ private fun ExchangeRateUnavailableButton(
if (transitionState.currentState || transitionState.targetState || !transitionState.isIdle) {
val offset = with(LocalDensity.current) { 78.dp.toPx() }.toInt()
UnavailableExchangeRatePopup(
StyledExchangeUnavailablePopup(
onDismissRequest = {
transitionState.targetState = false
},
@ -287,7 +306,7 @@ private fun DefaultPreview() =
modifier = Modifier,
zatoshi = Zatoshi(1),
state =
ExchangeRateState(
ExchangeRateState.Data(
isLoading = false,
currencyConversion =
FiatCurrencyConversion(
@ -313,7 +332,7 @@ private fun DefaultNoRefreshPreview() =
modifier = Modifier,
zatoshi = Zatoshi(1),
state =
ExchangeRateState(
ExchangeRateState.Data(
isLoading = false,
currencyConversion =
FiatCurrencyConversion(
@ -340,7 +359,7 @@ private fun HiddenPreview() =
modifier = Modifier,
zatoshi = Zatoshi(1),
state =
ExchangeRateState(
ExchangeRateState.Data(
isLoading = false,
currencyConversion =
FiatCurrencyConversion(
@ -366,7 +385,7 @@ private fun HiddenStalePreview() =
modifier = Modifier,
zatoshi = Zatoshi(1),
state =
ExchangeRateState(
ExchangeRateState.Data(
isLoading = false,
isStale = true,
currencyConversion =

View File

@ -0,0 +1,75 @@
package co.electriccoin.zcash.ui.screen.exchangerate.widget
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import cash.z.ecc.android.sdk.model.FiatCurrencyConversion
import cash.z.ecc.android.sdk.model.Zatoshi
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
import co.electriccoin.zcash.ui.design.component.BlankSurface
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.util.PreviewScreens
import co.electriccoin.zcash.ui.util.StringResource
import co.electriccoin.zcash.ui.util.stringRes
import kotlinx.datetime.Clock
@Suppress("LongParameterList", "ComplexCondition")
@Composable
fun StyledExchangeLabel(
zatoshi: Zatoshi,
state: ExchangeRateState,
modifier: Modifier = Modifier,
isHideBalances: Boolean = false,
hiddenBalancePlaceholder: StringResource =
stringRes(co.electriccoin.zcash.ui.design.R.string.hide_balance_placeholder),
style: TextStyle = ZcashTheme.typography.secondary.headlineSmall,
textColor: Color = ZcashTheme.colors.textFieldHint,
) {
when (state) {
is ExchangeRateState.Data ->
if (!state.isStale && state.currencyConversion != null) {
Text(
modifier = modifier,
text = createExchangeRateText(state, isHideBalances, hiddenBalancePlaceholder, zatoshi),
maxLines = 1,
color = textColor,
style = style,
)
}
is ExchangeRateState.OptIn -> {
// do not show anything
}
ExchangeRateState.OptedOut -> {
// do not show anything
}
}
}
@Suppress("UnusedPrivateMember")
@PreviewScreens
@Composable
private fun DefaultPreview() =
ZcashTheme {
BlankSurface {
StyledExchangeLabel(
isHideBalances = false,
modifier = Modifier,
zatoshi = Zatoshi(1),
state =
ExchangeRateState.Data(
isLoading = false,
isStale = false,
currencyConversion =
FiatCurrencyConversion(
timestamp = Clock.System.now(),
priceOfZec = 25.0
),
onRefresh = {}
)
)
}
}

View File

@ -0,0 +1,121 @@
package co.electriccoin.zcash.ui.screen.exchangerate.widget
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
import co.electriccoin.zcash.ui.design.component.BlankSurface
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.exchangerate.SecondaryCard
import co.electriccoin.zcash.ui.screen.exchangerate.ZashiButton
import co.electriccoin.zcash.ui.screen.exchangerate.ZashiButtonDefaults
import co.electriccoin.zcash.ui.util.PreviewScreens
@Suppress("LongMethod")
@Composable
fun StyledExchangeOptIn(
state: ExchangeRateState.OptIn,
modifier: Modifier = Modifier
) {
SecondaryCard(
modifier = modifier,
) {
Column(
modifier = Modifier.padding(start = 20.dp, bottom = 20.dp)
) {
Row {
Image(
modifier = Modifier.padding(top = 20.dp),
painter = painterResource(Icon),
contentDescription = ""
)
Spacer(modifier = Modifier.width(12.dp))
Column(
verticalArrangement = Arrangement.Center
) {
Spacer(modifier = Modifier.height(22.dp))
Text(
text = stringResource(R.string.exchange_rate_opt_in_title),
color = ZcashTheme.zashiColors.textTertiary,
fontSize = 14.sp,
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = stringResource(R.string.exchange_rate_opt_in_subtitle),
color = ZcashTheme.zashiColors.textPrimary,
fontSize = 16.sp,
style = ZcashTheme.extendedTypography.restoringTopAppBarStyle,
)
}
Spacer(modifier = Modifier.weight(1f))
IconButton(
modifier = Modifier.padding(top = 4.dp, end = 8.dp),
onClick = state.onDismissClick,
) {
Icon(
painter = painterResource(R.drawable.ic_exchange_rate_unavailable_dialog_close),
contentDescription = "",
tint = ZcashTheme.zashiColors.defaultFg
)
}
}
Spacer(modifier = Modifier.height(16.dp))
ZashiButton(
modifier =
Modifier
.fillMaxWidth()
.padding(end = 20.dp),
onClick = state.onPrimaryClick,
colors = ZashiButtonDefaults.tertiaryButtonColors()
) {
Text(
text = stringResource(R.string.exchange_rate_opt_in_primary_btn),
style = ZcashTheme.typography.primary.titleSmall.copy(fontWeight = FontWeight.SemiBold),
fontSize = 14.sp
)
}
}
}
}
private val Icon: Int
@DrawableRes
@Composable
get() =
if (isSystemInDarkTheme()) {
R.drawable.ic_exchange_rate_opt_in
} else {
R.drawable.ic_exchange_rate_opt_in_light
}
@Suppress("UnusedPrivateMember")
@Composable
@PreviewScreens
private fun ExchangeRateOptInPreview() =
ZcashTheme {
BlankSurface {
StyledExchangeOptIn(
modifier = Modifier.fillMaxWidth(),
state = ExchangeRateState.OptIn(onDismissClick = {})
)
}
}

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui
package co.electriccoin.zcash.ui.screen.exchangerate.widget
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.MutableTransitionState
@ -34,11 +34,12 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.util.PreviewScreens
@Composable
internal fun UnavailableExchangeRatePopup(
internal fun StyledExchangeUnavailablePopup(
offset: IntOffset,
transitionState: MutableTransitionState<Boolean>,
onDismissRequest: () -> Unit,
@ -93,7 +94,7 @@ private fun PopupContent(onDismissRequest: () -> Unit) {
color = ZcashTheme.zashiColors.textLight,
fontSize = 16.sp,
style = ZcashTheme.extendedTypography.restoringTopAppBarStyle,
text = stringResource(R.string.balances_exchange_rate_unavailable)
text = stringResource(R.string.exchange_rate_unavailable_title)
)
Spacer(modifier = Modifier.height(6.dp))
Text(
@ -103,12 +104,12 @@ private fun PopupContent(onDismissRequest: () -> Unit) {
fontWeight = FontWeight.Medium
),
fontSize = 14.sp,
text = stringResource(id = R.string.balances_exchange_rate_unavailable_subtitle)
text = stringResource(id = R.string.exchange_rate_unavailable_subtitle)
)
}
IconButton(onClick = onDismissRequest) {
Icon(
painter = painterResource(R.drawable.ic_unavailable_exchange_rate_dialog_close),
painter = painterResource(R.drawable.ic_exchange_rate_unavailable_dialog_close),
contentDescription = "",
tint = ZcashTheme.zashiColors.textLightSupport
)

View File

@ -45,7 +45,9 @@ sealed interface AmountState {
val zatoshi = Zatoshi.fromZecString(context, value, monetarySeparators)
val currencyConversion =
if (!exchangeRateState.isLoading && exchangeRateState.isStale) {
if (exchangeRateState !is ExchangeRateState.Data ||
(!exchangeRateState.isLoading && exchangeRateState.isStale)
) {
null
} else {
exchangeRateState.currencyConversion
@ -91,7 +93,7 @@ sealed interface AmountState {
}
val zatoshi =
exchangeRateState.currencyConversion?.toZatoshi(
(exchangeRateState as? ExchangeRateState.Data)?.currencyConversion?.toZatoshi(
context = context,
value = fiatValue,
monetarySeparators = MonetarySeparators.current(java.util.Locale.getDefault())

View File

@ -122,7 +122,7 @@ private fun PreviewSendForm() {
walletSnapshot = WalletSnapshotFixture.new(),
balanceState = BalanceStateFixture.new(),
isHideBalances = false,
exchangeRateState = ExchangeRateState {}
exchangeRateState = ExchangeRateState.OptedOut
)
}
}
@ -159,7 +159,7 @@ private fun SendFormTransparentAddressPreview() {
walletSnapshot = WalletSnapshotFixture.new(),
balanceState = BalanceStateFixture.new(),
isHideBalances = false,
exchangeRateState = ExchangeRateState {}
exchangeRateState = ExchangeRateState.OptedOut
)
}
}
@ -708,7 +708,9 @@ fun SendFormAmountTextField(
}
)
if (!exchangeRateState.isStale || exchangeRateState.isLoading) {
if (exchangeRateState is ExchangeRateState.Data &&
(!exchangeRateState.isStale || exchangeRateState.isLoading)
) {
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingMin))
Image(
modifier = Modifier.padding(top = 24.dp),

View File

@ -75,7 +75,7 @@ internal fun MainActivity.WrapSendConfirmation(
val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value
val exchangeRateState = walletViewModel.exchangeRateUsd.collectAsStateWithLifecycle().value
val exchangeRateState by remember { mutableStateOf(walletViewModel.exchangeRateUsd.value) }
WrapSendConfirmation(
activity = this,
@ -133,9 +133,13 @@ internal fun WrapSendConfirmation(
val onBackAction = {
when (stage) {
SendConfirmationStage.Confirmation -> goBack(false)
SendConfirmationStage.Sending -> { /* no action - wait until the sending is done */ }
SendConfirmationStage.Sending -> { // no action - wait until the sending is done
}
is SendConfirmationStage.Failure -> setStage(SendConfirmationStage.Confirmation)
is SendConfirmationStage.MultipleTrxFailure -> { /* no action - wait until report the result */ }
is SendConfirmationStage.MultipleTrxFailure -> { // no action - wait until report the result
}
is SendConfirmationStage.MultipleTrxFailureReported -> goBack(true)
}
}
@ -311,9 +315,11 @@ private fun processSubmissionResult(
setStage(SendConfirmationStage.Confirmation)
goHome()
}
is SubmitResult.SimpleTrxFailure -> {
setStage(SendConfirmationStage.Failure(submitResult.errorDescription))
}
is SubmitResult.MultipleTrxFailure -> {
setStage(SendConfirmationStage.MultipleTrxFailure)
}

View File

@ -39,7 +39,6 @@ import cash.z.ecc.sdk.extension.toZecStringFull
import cash.z.ecc.sdk.fixture.MemoFixture
import cash.z.ecc.sdk.fixture.ZatoshiFixture
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.StyledExchangeBalance
import co.electriccoin.zcash.ui.common.compose.BalanceWidgetBigLineOnly
import co.electriccoin.zcash.ui.common.extension.asZecAmountTriple
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
@ -61,6 +60,7 @@ import co.electriccoin.zcash.ui.design.component.Tiny
import co.electriccoin.zcash.ui.design.component.TopAppBarBackNavigation
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.ObserveFiatCurrencyResultFixture
import co.electriccoin.zcash.ui.screen.exchangerate.widget.StyledExchangeLabel
import co.electriccoin.zcash.ui.screen.sendconfirmation.SendConfirmationTag
import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SendConfirmationStage
import kotlinx.collections.immutable.ImmutableList
@ -393,12 +393,10 @@ private fun SendConfirmationContent(
isHideBalances = false
)
StyledExchangeBalance(
StyledExchangeLabel(
zatoshi = zecSend.amount,
state = exchangeRate,
isHideBalances = false,
style = ZcashTheme.typography.secondary.headlineSmall,
textColor = ZcashTheme.colors.textFieldHint
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))

View File

@ -2,6 +2,7 @@
<string name="advanced_settings_backup_wallet">Recovery phrase</string>
<string name="advanced_settings_export_private_data">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_delete_wallet">
Delete <xliff:g id="app_name" example="Zashi">%1$s</xliff:g>

View File

@ -48,7 +48,4 @@
<string name="balances_shielding_dialog_error_text">Error: The attempt to shield the transparent funds failed. Try it again, please.</string>
<string name="balances_shielding_dialog_error_btn">OK</string>
<string name="balances_shielding_dialog_error_below_threshold">The current transparent balance is zero or below the allowed shielding limit.</string>
<string name="balances_exchange_rate_unavailable">Exchange rate unavailable</string>
<string name="balances_exchange_rate_unavailable_subtitle">We tried but we couldn\t refresh the exchange rate
for you. Check your connection, relaunch the app, and well try again.</string>
</resources>

View File

@ -0,0 +1,52 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="192dp"
android:height="72dp"
android:viewportWidth="192"
android:viewportHeight="72">
<group>
<clip-path
android:pathData="M4,36C4,18.33 18.33,4 36,4C53.67,4 68,18.33 68,36C68,53.67 53.67,68 36,68C18.33,68 4,53.67 4,36Z"/>
<path
android:pathData="M36,4L36,4A32,32 0,0 1,68 36L68,36A32,32 0,0 1,36 68L36,68A32,32 0,0 1,4 36L4,36A32,32 0,0 1,36 4z"
android:fillColor="#231F20"/>
<path
android:pathData="M4,36C4,18.35 18.35,4 36,4C53.65,4 68,18.35 68,36C68,53.65 53.65,68 36,68C18.35,68 4,53.65 4,36ZM47.41,21.15V26.02L33.87,44.39H47.41V50.85H38.68V56.2H33.32V50.85H24.59V45.98L38.12,27.61H24.59V21.15H33.32V15.78H38.68V21.15H47.41Z"
android:fillColor="#FCBB1A"
android:fillType="evenOdd"/>
</group>
<path
android:pathData="M36,2C17.22,2 2,17.22 2,36C2,54.78 17.22,70 36,70C54.78,70 70,54.78 70,36C70,17.22 54.78,2 36,2Z"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#231F20"/>
<path
android:pathData="M96,2C77.22,2 62,17.22 62,36C62,54.78 77.22,70 96,70C114.78,70 130,54.78 130,36C130,17.22 114.78,2 96,2Z"
android:fillColor="#3B3839"/>
<path
android:pathData="M96,2C77.22,2 62,17.22 62,36C62,54.78 77.22,70 96,70C114.78,70 130,54.78 130,36C130,17.22 114.78,2 96,2Z"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#231F20"/>
<path
android:pathData="M106.67,42.67H85.33M85.33,42.67L90.67,37.33M85.33,42.67L90.67,48M85.33,29.33H106.67M106.67,29.33L101.33,24M106.67,29.33L101.33,34.67"
android:strokeLineJoin="round"
android:strokeWidth="2.66667"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"
android:strokeLineCap="round"/>
<path
android:pathData="M156,2C137.22,2 122,17.22 122,36C122,54.78 137.22,70 156,70C174.78,70 190,54.78 190,36C190,17.22 174.78,2 156,2Z"
android:fillColor="#067647"/>
<path
android:pathData="M156,2C137.22,2 122,17.22 122,36C122,54.78 137.22,70 156,70C174.78,70 190,54.78 190,36C190,17.22 174.78,2 156,2Z"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#231F20"/>
<path
android:pathData="M148,41.33C148,44.28 150.39,46.67 153.33,46.67H158.67C161.61,46.67 164,44.28 164,41.33C164,38.39 161.61,36 158.67,36H153.33C150.39,36 148,33.61 148,30.67C148,27.72 150.39,25.33 153.33,25.33H158.67C161.61,25.33 164,27.72 164,30.67M156,22.67V49.33"
android:strokeLineJoin="round"
android:strokeWidth="2.66667"
android:fillColor="#00000000"
android:strokeColor="#ABEFC6"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,52 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="204dp"
android:height="83dp"
android:viewportWidth="204"
android:viewportHeight="83">
<group>
<clip-path
android:pathData="M10,39C10,21.33 24.33,7 42,7C59.67,7 74,21.33 74,39C74,56.67 59.67,71 42,71C24.33,71 10,56.67 10,39Z"/>
<path
android:pathData="M42,7L42,7A32,32 0,0 1,74 39L74,39A32,32 0,0 1,42 71L42,71A32,32 0,0 1,10 39L10,39A32,32 0,0 1,42 7z"
android:fillColor="#ffffff"/>
<path
android:pathData="M10,39C10,21.35 24.35,7 42,7C59.65,7 74,21.35 74,39C74,56.65 59.65,71 42,71C24.35,71 10,56.65 10,39ZM53.41,24.15V29.02L39.87,47.39H53.41V53.85H44.68V59.2H39.32V53.85H30.58V48.98L44.12,30.61H30.58V24.15H39.32V18.78H44.68V24.15H53.41Z"
android:fillColor="#FCBB1A"
android:fillType="evenOdd"/>
</group>
<path
android:pathData="M42,5C23.22,5 8,20.22 8,39C8,57.78 23.22,73 42,73C60.78,73 76,57.78 76,39C76,20.22 60.78,5 42,5Z"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#ffffff"/>
<path
android:pathData="M70,39C70,21.33 84.33,7 102,7C119.67,7 134,21.33 134,39C134,56.67 119.67,71 102,71C84.33,71 70,56.67 70,39Z"
android:fillColor="#F4F4F4"/>
<path
android:pathData="M102,5C83.22,5 68,20.22 68,39C68,57.78 83.22,73 102,73C120.78,73 136,57.78 136,39C136,20.22 120.78,5 102,5Z"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#ffffff"/>
<path
android:pathData="M112.67,45.67H91.33M91.33,45.67L96.67,40.33M91.33,45.67L96.67,51M91.33,32.33H112.67M112.67,32.33L107.33,27M112.67,32.33L107.33,37.67"
android:strokeLineJoin="round"
android:strokeWidth="2.66667"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
<path
android:pathData="M130,39C130,21.33 144.33,7 162,7C179.67,7 194,21.33 194,39C194,56.67 179.67,71 162,71C144.33,71 130,56.67 130,39Z"
android:fillColor="#75E0A7"/>
<path
android:pathData="M162,5C143.22,5 128,20.22 128,39C128,57.78 143.22,73 162,73C180.78,73 196,57.78 196,39C196,20.22 180.78,5 162,5Z"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#ffffff"/>
<path
android:pathData="M154,44.33C154,47.28 156.39,49.67 159.33,49.67H164.67C167.61,49.67 170,47.28 170,44.33C170,41.39 167.61,39 164.67,39H159.33C156.39,39 154,36.61 154,33.67C154,30.72 156.39,28.33 159.33,28.33H164.67C167.61,28.33 170,30.72 170,33.67M162,25.67V52.33"
android:strokeLineJoin="round"
android:strokeWidth="2.66667"
android:fillColor="#00000000"
android:strokeColor="#085D3A"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M0.5,10C0.5,4.753 4.753,0.5 10,0.5C15.247,0.5 19.5,4.753 19.5,10C19.5,15.247 15.247,19.5 10,19.5C4.753,19.5 0.5,15.247 0.5,10Z"
android:fillColor="#E8E8E8"/>
<path
android:strokeWidth="1"
android:pathData="M0.5,10C0.5,4.753 4.753,0.5 10,0.5C15.247,0.5 19.5,4.753 19.5,10C19.5,15.247 15.247,19.5 10,19.5C4.753,19.5 0.5,15.247 0.5,10Z"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"/>
<path
android:pathData="M10,6L10,6A4,4 0,0 1,14 10L14,10A4,4 0,0 1,10 14L10,14A4,4 0,0 1,6 10L6,10A4,4 0,0 1,10 6z"
android:fillColor="#343031"/>
</vector>

View File

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M0.5,10C0.5,4.753 4.753,0.5 10,0.5C15.247,0.5 19.5,4.753 19.5,10C19.5,15.247 15.247,19.5 10,19.5C4.753,19.5 0.5,15.247 0.5,10Z"
android:fillColor="#231F20"/>
<path
android:strokeWidth="1"
android:pathData="M0.5,10C0.5,4.753 4.753,0.5 10,0.5C15.247,0.5 19.5,4.753 19.5,10C19.5,15.247 15.247,19.5 10,19.5C4.753,19.5 0.5,15.247 0.5,10Z"
android:fillColor="#00000000"
android:strokeColor="#231F20"/>
<path
android:pathData="M10,6L10,6A4,4 0,0 1,14 10L14,10A4,4 0,0 1,10 14L10,14A4,4 0,0 1,6 10L6,10A4,4 0,0 1,10 6z"
android:fillColor="#ffffff"/>
</vector>

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M0.5,10C0.5,4.753 4.753,0.5 10,0.5C15.247,0.5 19.5,4.753 19.5,10C19.5,15.247 15.247,19.5 10,19.5C4.753,19.5 0.5,15.247 0.5,10Z"
android:fillColor="#343031"/>
<path
android:strokeWidth="1"
android:pathData="M0.5,10C0.5,4.753 4.753,0.5 10,0.5C15.247,0.5 19.5,4.753 19.5,10C19.5,15.247 15.247,19.5 10,19.5C4.753,19.5 0.5,15.247 0.5,10Z"
android:fillColor="#00000000"
android:strokeColor="#939091"/>
</vector>

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M0.5,10C0.5,4.753 4.753,0.5 10,0.5C15.247,0.5 19.5,4.753 19.5,10C19.5,15.247 15.247,19.5 10,19.5C4.753,19.5 0.5,15.247 0.5,10Z"
android:fillColor="#ffffff"/>
<path
android:strokeWidth="1"
android:pathData="M0.5,10C0.5,4.753 4.753,0.5 10,0.5C15.247,0.5 19.5,4.753 19.5,10C19.5,15.247 15.247,19.5 10,19.5C4.753,19.5 0.5,15.247 0.5,10Z"
android:fillColor="#00000000"
android:strokeColor="#C0BFB1"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="14dp"
android:height="14dp"
android:viewportWidth="14"
android:viewportHeight="14">
<path
android:pathData="M13,1L1,13M1,1L13,13"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#D2D1D2"
android:strokeLineCap="round"/>
</vector>

View File

@ -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="M17.5,19.583L19.166,21.25L22.916,17.5M26.666,20C26.666,24.09 22.205,27.065 20.581,28.013C20.397,28.12 20.305,28.174 20.174,28.202C20.073,28.223 19.926,28.223 19.825,28.202C19.695,28.174 19.603,28.12 19.418,28.013C17.795,27.065 13.333,24.09 13.333,20V16.015C13.333,15.349 13.333,15.015 13.442,14.729C13.538,14.476 13.695,14.25 13.898,14.071C14.128,13.869 14.439,13.752 15.063,13.518L19.531,11.842C19.705,11.777 19.791,11.745 19.881,11.732C19.959,11.72 20.04,11.72 20.119,11.732C20.208,11.745 20.295,11.777 20.468,11.842L24.936,13.518C25.56,13.752 25.872,13.869 26.102,14.071C26.305,14.25 26.461,14.476 26.557,14.729C26.666,15.015 26.666,15.349 26.666,16.015V20Z"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"
android:strokeLineCap="round"/>
</vector>

View File

@ -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="M17.5,19.583L19.167,21.25L22.917,17.5M26.667,20C26.667,24.09 22.205,27.065 20.582,28.013C20.397,28.12 20.305,28.174 20.175,28.202C20.074,28.223 19.927,28.223 19.825,28.202C19.695,28.174 19.603,28.12 19.419,28.013C17.795,27.065 13.333,24.09 13.333,20V16.015C13.333,15.349 13.333,15.015 13.443,14.729C13.539,14.476 13.695,14.25 13.898,14.071C14.128,13.869 14.44,13.752 15.064,13.518L19.532,11.842C19.705,11.777 19.792,11.745 19.881,11.732C19.96,11.72 20.04,11.72 20.119,11.732C20.208,11.745 20.295,11.777 20.468,11.842L24.937,13.518C25.56,13.752 25.872,13.869 26.102,14.071C26.305,14.25 26.462,14.476 26.558,14.729C26.667,15.015 26.667,15.349 26.667,16.015V20Z"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</vector>

View File

@ -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="M27.044,20.744C26.813,22.919 25.58,24.957 23.541,26.135C20.153,28.091 15.821,26.93 13.865,23.542L13.657,23.181M12.955,19.256C13.187,17.081 14.419,15.043 16.458,13.866C19.846,11.91 24.178,13.071 26.134,16.458L26.343,16.819M12.911,25.055L13.521,22.778L15.798,23.389M24.202,16.612L26.479,17.222L27.089,14.945"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"
android:strokeLineCap="round"/>
</vector>

View File

@ -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="M27.044,20.744C26.813,22.919 25.58,24.957 23.541,26.135C20.153,28.091 15.821,26.93 13.865,23.542L13.657,23.181M12.955,19.256C13.187,17.081 14.419,15.043 16.458,13.866C19.846,11.91 24.178,13.071 26.134,16.458L26.343,16.819M12.911,25.055L13.521,22.778L15.798,23.389M24.202,16.612L26.479,17.222L27.089,14.945"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,20 @@
<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"/>
<group>
<clip-path
android:pathData="M10,10h20v20h-20z"/>
<path
android:pathData="M15,15L16.667,13.333M16.667,13.333L15,11.667M16.667,13.333H15C13.159,13.333 11.667,14.826 11.667,16.667M25,25L23.333,26.667M23.333,26.667L25,28.333M23.333,26.667H25C26.841,26.667 28.333,25.174 28.333,23.333M21.181,21.181C21.833,21.492 22.563,21.667 23.333,21.667C26.095,21.667 28.333,19.428 28.333,16.667C28.333,13.905 26.095,11.667 23.333,11.667C20.572,11.667 18.333,13.905 18.333,16.667C18.333,17.437 18.507,18.167 18.819,18.819M21.667,23.333C21.667,26.095 19.428,28.333 16.667,28.333C13.905,28.333 11.667,26.095 11.667,23.333C11.667,20.572 13.905,18.333 16.667,18.333C19.428,18.333 21.667,20.572 21.667,23.333Z"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@ -0,0 +1,20 @@
<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"/>
<group>
<clip-path
android:pathData="M10,10h20v20h-20z"/>
<path
android:pathData="M15,15L16.667,13.333M16.667,13.333L15,11.667M16.667,13.333H15C13.159,13.333 11.667,14.826 11.667,16.667M25,25L23.333,26.667M23.333,26.667L25,28.333M23.333,26.667H25C26.841,26.667 28.333,25.174 28.333,23.333M21.181,21.181C21.833,21.492 22.563,21.667 23.333,21.667C26.095,21.667 28.333,19.428 28.333,16.667C28.333,13.905 26.095,11.667 23.333,11.667C20.572,11.667 18.333,13.905 18.333,16.667C18.333,17.437 18.507,18.167 18.819,18.819M21.667,23.333C21.667,26.095 19.428,28.333 16.667,28.333C13.905,28.333 11.667,26.095 11.667,23.333C11.667,20.572 13.905,18.333 16.667,18.333C19.428,18.333 21.667,20.572 21.667,23.333Z"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@ -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="M26.666,15L17.5,24.167L13.333,20"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"
android:strokeLineCap="round"/>
</vector>

View File

@ -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="M26.667,15L17.5,24.167L13.333,20"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</vector>

View File

@ -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="M25,15L15,25M15,15L25,25"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"
android:strokeLineCap="round"/>
</vector>

View File

@ -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="M25,15L15,25M15,15L25,25"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,30 @@
<resources>
<string name="exchange_rate_info_title_1">IP Address Protection</string>
<string name="exchange_rate_info_subtitle_1">Zashi\s currency conversion feature doesn\t compromise your IP
address.</string>
<string name="exchange_rate_info_title_2">Rate Refresh</string>
<string name="exchange_rate_info_subtitle_2">The rate is refreshed automatically and can also be refreshed manually.</string>
<string name="exchange_rate_unavailable_title">Exchange rate unavailable</string>
<string name="exchange_rate_unavailable_subtitle">We tried but we couldn\t refresh the exchange rate
for you. Check your connection, relaunch the app, and we\ll try again.</string>
<string name="exchange_rate_opt_in_title">New Feature</string>
<string name="exchange_rate_opt_in_subtitle">Currency Conversion</string>
<string name="exchange_rate_opt_in_primary_btn">Review</string>
<string name="exchange_rate_opt_in_enable">Enable</string>
<string name="exchange_rate_opt_in_save">Save changes</string>
<string name="exchange_rate_opt_in_skip">Skip for now</string>
<string name="exchange_rate_opt_in_note">Note for the super privacy-conscious: Because we pull the conversion rate
from exchanges, an exchange might be able to see that the exchange rate was queried before a transaction occurred.</string>
<string name="exchange_rate_opt_in_option_title">Enable</string>
<string name="exchange_rate_opt_in_option_subtitle">Show me the currency conversion.</string>
<string name="exchange_rate_opt_out_option_title">Disable</string>
<string name="exchange_rate_opt_out_option_subtitle">Don\t show the currency conversion.</string>
<string name="exchange_rate_opt_in_description">Display your balance and payment amounts in USD.\nYou can manage
this feature in Advanced Settings.</string>
<string name="exchange_rate_opt_in_description_settings">Display your balance and payment amounts in USD.\n
Zashi\s currency conversion feature protects your IP address at all times.</string>
</resources>