Issue 1617 not enough space and update redesign (#1652)

* [#1617] Update redesign

Closes ##1617

* [#1617] Not enough space redesign

Closes ##1617

* [#1617] Code cleanup

Closes ##1617

* [#1617] Documentation update

Closes ##1617

* [#1617] Sensitive settings disabled during needed update

Closes ##1617

* Code cleanup

* Test hotfix

* Test hotfix

* Test hotfix

* Address review comments

* Fix broken in-app update logic

- It was broken since we introduced Koin
- Now fixed and tested using AppUpdateCheckerMock

* Changelogs update

---------

Co-authored-by: Honza <rychnovsky.honza@gmail.com>
This commit is contained in:
Milan 2024-11-12 15:27:50 +01:00 committed by GitHub
parent d97956de44
commit c6350641e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 708 additions and 731 deletions

View File

@ -11,10 +11,14 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
- Zashi app now supports Spanish language
- The Flexa SDK has been adopted to enable payments using the embedded Flexa UI
### Changed
- The Not enough space and In-app udpate screens have been redesigned
### Fixed
- Address book toast now correctly shows on send screen when adding both new and known addresses to text field
- The application now correctly navigates to the homepage after deleting the current wallet and creating a new or
recovering an older one
- The in-app update logic has been fixed and is now correctly requested with every app launch
## [1.2.1 (760)] - 2024-10-22

View File

@ -14,10 +14,14 @@ directly impact users rather than highlighting other key architectural updates.*
- Zashi app now supports Spanish language
- The Flexa SDK has been adopted to enable payments using the embedded Flexa UI
### Changed
- The Not enough space and In-app udpate screens have been redesigned
### Fixed
- Address book toast now correctly shows on send screen when adding both new and known addresses to text field
- The application now correctly navigates to the homepage after deleting the current wallet and creating a new or
recovering an older one
- The in-app update logic has been fixed and is now correctly requested with every app launch
## [1.2.1 (760)] - 2024-10-22

View File

@ -18,3 +18,7 @@ directly impact users rather than highlighting other key architectural updates.*
- Address book toast now correctly shows on send screen when adding both new and known addresses to text field
- The application now correctly navigates to the homepage after deleting the current wallet and creating a new or
recovering an older one
- The in-app update logic has been fixed and is now correctly requested with every app launch
### Changed
- The Not enough space and In-app udpate screens have been redesigned

View File

@ -1,35 +0,0 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.R
import co.electriccoin.zcash.ui.design.util.orDark
@Composable
fun ZashiTopAppBarBackNavigation(
backContentDescriptionText: String = stringResource(R.string.back_navigation_content_description),
painter: Painter =
painterResource(
R.drawable.ic_zashi_navigation_back orDark R.drawable.ic_zashi_navigation_back_dark
),
onBack: () -> Unit
) {
Row {
Spacer(modifier = Modifier.width(16.dp))
IconButton(onClick = onBack) {
Icon(
painter = painter,
contentDescription = backContentDescriptionText,
)
}
}
}

View File

@ -0,0 +1,71 @@
package co.electriccoin.zcash.ui.design.component
import androidx.annotation.DrawableRes
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.R
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
@Composable
fun ZashiTopAppBarBackNavigation(
onBack: () -> Unit,
modifier: Modifier = Modifier
) = ZashiTopAppBarNavigation(
modifier = modifier,
backContentDescriptionText = stringResource(R.string.back_navigation_content_description),
drawableRes = R.drawable.ic_zashi_navigation_back,
onBack = onBack
)
@Composable
fun ZashiTopAppBarCloseNavigation(
onBack: () -> Unit,
modifier: Modifier = Modifier
) = ZashiTopAppBarNavigation(
modifier = modifier,
backContentDescriptionText = stringResource(R.string.back_navigation_content_description),
drawableRes = R.drawable.ic_navigation_close,
onBack = onBack,
tint = ZashiColors.Text.textPrimary
)
@Composable
fun ZashiTopAppBarHamburgerNavigation(onBack: () -> Unit) =
ZashiTopAppBarNavigation(
backContentDescriptionText = stringResource(R.string.back_navigation_content_description),
drawableRes = R.drawable.ic_navigation_hamburger,
onBack = onBack,
tint = ZashiColors.Text.textPrimary
)
@Composable
private fun ZashiTopAppBarNavigation(
backContentDescriptionText: String,
@DrawableRes drawableRes: Int,
onBack: () -> Unit,
modifier: Modifier = Modifier,
tint: Color? = null,
) {
Row(
modifier = modifier,
) {
Spacer(modifier = Modifier.width(16.dp))
IconButton(onClick = onBack) {
Icon(
painter = painterResource(drawableRes),
contentDescription = backContentDescriptionText,
tint = tint ?: LocalContentColor.current
)
}
}
}

View File

@ -0,0 +1,20 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.util.orDark
@Composable
fun zashiVerticalGradient(
startColor: Color = ZashiColors.Utility.WarningYellow.utilityOrange100,
endColor: Color = ZashiColors.Surfaces.bgPrimary
) = Brush.verticalGradient(
START_STOP to startColor,
(END_STOP_LIGHT orDark END_STOP_DARK) to endColor,
)
private const val START_STOP = .0f
private const val END_STOP_DARK = .35f
private const val END_STOP_LIGHT = .4f

View File

@ -0,0 +1,13 @@
<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="M26,14L14,26M14,14L26,26"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,13 @@
<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="M11,20H29M11,14H29M11,26H29"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</vector>

View File

@ -35,7 +35,7 @@ class UpdateViewModelTest : UiTestPrerequisites() {
@Before
fun setup() {
checker = AppUpdateCheckerMock.new()
checker = AppUpdateCheckerMock()
initialUpdateInfo =
UpdateInfoFixture.new(

View File

@ -106,7 +106,6 @@ class SettingsViewTestSetup(
Settings(
state =
SettingsState(
isLoading = false,
version = stringRes("app_version"),
debugMenu = settingsTroubleshootingState,
onBack = {

View File

@ -41,13 +41,13 @@ class UpdateViewAndroidTest : UiTestPrerequisites() {
)
newTestSetup(updateInfo)
composeTestRule.onNodeWithText(getStringResource(R.string.update_header), ignoreCase = true).also {
composeTestRule.onNodeWithText(getStringResource(R.string.update_title_available), ignoreCase = true).also {
it.assertExists()
}
Espresso.pressBack()
composeTestRule.onNodeWithText(getStringResource(R.string.update_header), ignoreCase = true).also {
composeTestRule.onNodeWithText(getStringResource(R.string.update_title_available), ignoreCase = true).also {
it.assertDoesNotExist()
}
}
@ -64,13 +64,13 @@ class UpdateViewAndroidTest : UiTestPrerequisites() {
)
newTestSetup(updateInfo)
composeTestRule.onNodeWithText(getStringResource(R.string.update_critical_header), ignoreCase = true).also {
composeTestRule.onNodeWithText(getStringResource(R.string.update_title_required), ignoreCase = true).also {
it.assertExists()
}
Espresso.pressBack()
composeTestRule.onNodeWithText(getStringResource(R.string.update_critical_header), ignoreCase = true).also {
composeTestRule.onNodeWithText(getStringResource(R.string.update_title_required), ignoreCase = true).also {
it.assertExists()
}
}

View File

@ -19,7 +19,7 @@ class UpdateViewAndroidTestSetup(
UpdateViewModel(
application = composeTestRule.activity.application,
updateInfo = updateInfo,
appUpdateChecker = AppUpdateCheckerMock.new()
appUpdateChecker = AppUpdateCheckerMock()
)
@Composable
@ -32,7 +32,8 @@ class UpdateViewAndroidTestSetup(
updateInfo = updateInfo,
checkForUpdate = viewModel::checkForAppUpdate,
remindLater = viewModel::remindLater,
goForUpdate = {}
goForUpdate = {},
onSettings = {}
)
}
}

View File

@ -6,7 +6,6 @@ import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.test.filters.MediumTest
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
@ -17,6 +16,7 @@ import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Assert.assertEquals
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@ -38,14 +38,7 @@ class UpdateViewTest : UiTestPrerequisites() {
newTestSetup(updateInfo)
composeTestRule.onNodeWithText(
text = getStringResource(R.string.update_critical_header),
ignoreCase = true
).also {
it.assertExists()
}
composeTestRule.onNodeWithText(
text = getStringResource(R.string.update_later_disabled_button),
text = getStringResource(R.string.update_title_required),
ignoreCase = true
).also {
it.assertExists()
@ -85,14 +78,7 @@ class UpdateViewTest : UiTestPrerequisites() {
newTestSetup(updateInfo)
composeTestRule.onNodeWithText(
text = getStringResource(R.string.update_header),
ignoreCase = true
).also {
it.assertExists()
}
composeTestRule.onNodeWithText(
text = getStringResource(R.string.update_later_enabled_button),
text = getStringResource(R.string.update_title_available),
ignoreCase = true
).also {
it.assertExists()
@ -101,11 +87,11 @@ class UpdateViewTest : UiTestPrerequisites() {
@Test
@MediumTest
fun later_btn_force_update_test() {
fun later_btn_update_test() {
val updateInfo =
UpdateInfoFixture.new(
priority = AppUpdateChecker.Priority.HIGH,
force = true,
priority = AppUpdateChecker.Priority.LOW,
force = false,
appUpdateInfo = null,
state = UpdateState.Prepared
)
@ -115,7 +101,7 @@ class UpdateViewTest : UiTestPrerequisites() {
composeTestRule.clickLater()
assertEquals(0, testSetup.getOnLaterCount())
assertEquals(1, testSetup.getOnLaterCount())
}
@Test
@ -127,10 +113,6 @@ class UpdateViewTest : UiTestPrerequisites() {
assertEquals(0, testSetup.getOnDownloadCount())
composeTestRule.onNodeWithText(UpdateTag.PROGRESSBAR_DOWNLOADING).also {
it.assertDoesNotExist()
}
composeTestRule.clickDownload()
assertEquals(1, testSetup.getOnDownloadCount())
@ -138,6 +120,7 @@ class UpdateViewTest : UiTestPrerequisites() {
@Test
@MediumTest
@Ignore("Disable the test for now -> we have no way to click a clickable span right now")
fun play_store_ref_test() {
val updateInfo = UpdateInfoFixture.new(appUpdateInfo = null)
@ -146,9 +129,8 @@ class UpdateViewTest : UiTestPrerequisites() {
assertEquals(0, testSetup.getOnReferenceCount())
composeTestRule.onRoot().assertExists()
composeTestRule.onNodeWithText(getStringResource(R.string.update_link_text)).also {
composeTestRule.onNodeWithText(getStringResource(R.string.update_link_text), substring = true,).also {
it.assertExists()
it.performScrollTo()
it.performClick()
}

View File

@ -58,7 +58,8 @@ class UpdateViewTestSetup(
},
onReference = {
onReferenceCount.incrementAndGet()
}
},
onSettings = {}
)
}

View File

@ -24,6 +24,7 @@ import co.electriccoin.zcash.ui.common.usecase.PersistEndpointUseCase
import co.electriccoin.zcash.ui.common.usecase.RefreshFastestServersUseCase
import co.electriccoin.zcash.ui.common.usecase.RescanBlockchainUseCase
import co.electriccoin.zcash.ui.common.usecase.SaveContactUseCase
import co.electriccoin.zcash.ui.common.usecase.SensitiveSettingsVisibleUseCase
import co.electriccoin.zcash.ui.common.usecase.ShareImageUseCase
import co.electriccoin.zcash.ui.common.usecase.UpdateContactUseCase
import co.electriccoin.zcash.ui.common.usecase.ValidateContactAddressUseCase
@ -69,4 +70,5 @@ val useCaseModule =
singleOf(::ObserveWalletStateUseCase)
singleOf(::IsCoinbaseAvailableUseCase)
singleOf(::GetSpendingKeyUseCase)
singleOf(::SensitiveSettingsVisibleUseCase)
}

View File

@ -26,6 +26,7 @@ import co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel.CreateTransact
import co.electriccoin.zcash.ui.screen.settings.viewmodel.ScreenBrightnessViewModel
import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel
import co.electriccoin.zcash.ui.screen.support.viewmodel.SupportViewModel
import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo
import co.electriccoin.zcash.ui.screen.update.viewmodel.UpdateViewModel
import co.electriccoin.zcash.ui.screen.warning.viewmodel.StorageCheckViewModel
import co.electriccoin.zcash.ui.screen.whatsnew.viewmodel.WhatsNewViewModel
@ -50,7 +51,13 @@ val viewModelModule =
viewModelOf(::CreateTransactionsViewModel)
viewModelOf(::RestoreSuccessViewModel)
viewModelOf(::WhatsNewViewModel)
viewModelOf(::UpdateViewModel)
viewModel { (updateInfo: UpdateInfo) ->
UpdateViewModel(
application = get(),
updateInfo = updateInfo,
appUpdateChecker = get(),
)
}
viewModelOf(::ChooseServerViewModel)
viewModel { (args: AddressBookArgs) ->
AddressBookViewModel(

View File

@ -510,7 +510,7 @@ private fun fillInHandleForPaymentRequest(
handle[PAYMENT_REQUEST_URI] = zip321
}
private fun NavHostController.navigateJustOnce(
fun NavHostController.navigateJustOnce(
route: String,
navOptionsBuilder: (NavOptionsBuilder.() -> Unit)? = null
) {

View File

@ -0,0 +1,30 @@
package co.electriccoin.zcash.ui.common.usecase
import android.content.Context
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
class SensitiveSettingsVisibleUseCase(
appUpdateChecker: AppUpdateChecker,
context: Context
) {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
private val flow =
appUpdateChecker.newCheckForUpdateAvailabilityFlow(context)
.map { it.isForce.not() }
.stateIn(
scope = scope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = true
)
operator fun invoke() = flow
}

View File

@ -14,7 +14,6 @@ import co.electriccoin.zcash.ui.screen.advancedsettings.viewmodel.AdvancedSettin
import kotlinx.collections.immutable.toImmutableList
import org.koin.androidx.compose.koinViewModel
@Suppress("LongParameterList")
@Composable
internal fun WrapAdvancedSettings(
goDeleteWallet: () -> Unit,

View File

@ -2,55 +2,71 @@ package co.electriccoin.zcash.ui.screen.advancedsettings.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.NavigationTargets
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.usecase.SensitiveSettingsVisibleUseCase
import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItemState
import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.screen.advancedsettings.model.AdvancedSettingsState
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class AdvancedSettingsViewModel : ViewModel() {
val state =
MutableStateFlow(
AdvancedSettingsState(
onBack = ::onBack,
items =
persistentListOf(
ZashiSettingsListItemState(
text = stringRes(R.string.advanced_settings_recovery),
icon = R.drawable.ic_advanced_settings_recovery,
onClick = {}
),
ZashiSettingsListItemState(
text = stringRes(R.string.advanced_settings_export),
icon = R.drawable.ic_advanced_settings_export,
onClick = {}
),
ZashiSettingsListItemState(
text = stringRes(R.string.advanced_settings_choose_server),
icon =
R.drawable.ic_advanced_settings_choose_server,
onClick = ::onChooseServerClick
),
ZashiSettingsListItemState(
text = stringRes(R.string.advanced_settings_currency_conversion),
icon =
R.drawable.ic_advanced_settings_currency_conversion,
onClick = ::onCurrencyConversionClick
)
),
deleteButton =
ButtonState(
stringRes(R.string.advanced_settings_delete_button),
onClick = {}
)
class AdvancedSettingsViewModel(
isSensitiveSettingsVisible: SensitiveSettingsVisibleUseCase
) : ViewModel() {
val state: StateFlow<AdvancedSettingsState> =
isSensitiveSettingsVisible()
.map { isSensitiveSettingsVisible ->
createState(isSensitiveSettingsVisible)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = createState(isSensitiveSettingsVisible().value)
)
).asStateFlow()
private fun createState(isSensitiveSettingsVisible: Boolean) =
AdvancedSettingsState(
onBack = ::onBack,
items =
listOfNotNull(
ZashiSettingsListItemState(
text = stringRes(R.string.advanced_settings_recovery),
icon = R.drawable.ic_advanced_settings_recovery,
onClick = {}
),
ZashiSettingsListItemState(
text = stringRes(R.string.advanced_settings_export),
icon = R.drawable.ic_advanced_settings_export,
onClick = {}
),
ZashiSettingsListItemState(
text = stringRes(R.string.advanced_settings_choose_server),
icon =
R.drawable.ic_advanced_settings_choose_server,
onClick = ::onChooseServerClick
).takeIf { isSensitiveSettingsVisible },
ZashiSettingsListItemState(
text = stringRes(R.string.advanced_settings_currency_conversion),
icon =
R.drawable.ic_advanced_settings_currency_conversion,
onClick = ::onCurrencyConversionClick
).takeIf { isSensitiveSettingsVisible }
).toImmutableList(),
deleteButton =
ButtonState(
stringRes(R.string.advanced_settings_delete_button),
onClick = {}
),
)
val navigationCommand = MutableSharedFlow<String>()
val backNavigationCommand = MutableSharedFlow<Unit>()

View File

@ -36,10 +36,8 @@ internal fun WrapSettings() {
settingsViewModel.onBack()
}
state?.let {
Settings(
state = it,
topAppBarSubTitleState = walletState,
)
}
Settings(
state = state,
topAppBarSubTitleState = walletState,
)
}

View File

@ -6,7 +6,6 @@ import kotlinx.collections.immutable.ImmutableList
data class SettingsState(
val version: StringResource,
val isLoading: Boolean,
val onBack: () -> Unit,
val debugMenu: SettingsTroubleshootingState?,
val items: ImmutableList<ZashiSettingsListItemState>,

View File

@ -29,7 +29,6 @@ import androidx.compose.ui.unit.dp
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.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
@ -59,31 +58,27 @@ fun Settings(
)
}
) { paddingValues ->
if (state.isLoading) {
CircularScreenProgressIndicator()
} else {
Column(
modifier =
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(
top = paddingValues.calculateTopPadding(),
bottom = paddingValues.calculateBottomPadding() + ZashiDimensions.Spacing.spacing3xl,
start = 4.dp,
end = 4.dp
),
) {
state.items.forEachIndexed { index, item ->
ZashiSettingsListItem(state = item)
if (index != state.items.lastIndex) {
ZashiHorizontalDivider()
}
Column(
modifier =
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(
top = paddingValues.calculateTopPadding(),
bottom = paddingValues.calculateBottomPadding() + ZashiDimensions.Spacing.spacing3xl,
start = 4.dp,
end = 4.dp
),
) {
state.items.forEachIndexed { index, item ->
ZashiSettingsListItem(state = item)
if (index != state.items.lastIndex) {
ZashiHorizontalDivider()
}
Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacingXl))
Spacer(modifier = Modifier.weight(1f))
ZashiVersion(modifier = Modifier.align(CenterHorizontally), version = state.version)
}
Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacingXl))
Spacer(modifier = Modifier.weight(1f))
ZashiVersion(modifier = Modifier.align(CenterHorizontally), version = state.version)
}
}
}
@ -197,53 +192,6 @@ private fun PreviewSettings() {
Settings(
state =
SettingsState(
isLoading = false,
version = stringRes("Version 1.2"),
debugMenu = null,
onBack = {},
items =
persistentListOf(
ZashiSettingsListItemState(
text = stringRes(R.string.settings_address_book),
icon = R.drawable.ic_settings_address_book,
onClick = { },
),
ZashiSettingsListItemState(
text = stringRes(R.string.settings_integrations),
icon = R.drawable.ic_settings_integrations,
onClick = { },
titleIcons = persistentListOf(R.drawable.ic_integrations_coinbase)
),
ZashiSettingsListItemState(
text = stringRes(R.string.settings_advanced_settings),
icon = R.drawable.ic_advanced_settings,
onClick = { },
),
ZashiSettingsListItemState(
text = stringRes(R.string.settings_about_us),
icon = R.drawable.ic_settings_info,
onClick = { },
),
ZashiSettingsListItemState(
text = stringRes(R.string.settings_feedback),
icon = R.drawable.ic_settings_feedback,
onClick = { },
),
),
),
topAppBarSubTitleState = TopAppBarSubTitleState.None,
)
}
}
@PreviewScreens
@Composable
private fun PreviewSettingsLoading() {
ZcashTheme {
Settings(
state =
SettingsState(
isLoading = true,
version = stringRes("Version 1.2"),
debugMenu = null,
onBack = {},

View File

@ -14,6 +14,7 @@ import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider
import co.electriccoin.zcash.ui.common.usecase.IsFlexaAvailableUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase
import co.electriccoin.zcash.ui.common.usecase.RescanBlockchainUseCase
import co.electriccoin.zcash.ui.common.usecase.SensitiveSettingsVisibleUseCase
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItemState
import co.electriccoin.zcash.ui.design.util.stringRes
@ -22,14 +23,12 @@ 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.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
@ -38,6 +37,7 @@ import kotlinx.coroutines.launch
@Suppress("TooManyFunctions")
class SettingsViewModel(
observeConfiguration: ObserveConfigurationUseCase,
isSensitiveSettingsVisible: SensitiveSettingsVisibleUseCase,
private val standardPreferenceProvider: StandardPreferenceProvider,
private val getVersionInfo: GetVersionInfoProvider,
private val rescanBlockchain: RescanBlockchainUseCase,
@ -50,15 +50,6 @@ class SettingsViewModel(
private val isKeepScreenOnWhileSyncingEnabled =
booleanStateFlow(StandardPreferenceKeys.IS_KEEP_SCREEN_ON_DURING_SYNC)
private val isLoading =
combine(
isAnalyticsEnabled,
isBackgroundSyncEnabled,
isKeepScreenOnWhileSyncingEnabled
) { isAnalyticsEnabled, isBackgroundSync, isKeepScreenOnWhileSyncing ->
isAnalyticsEnabled == null || isBackgroundSync == null || isKeepScreenOnWhileSyncing == null
}.distinctUntilChanged()
@Suppress("ComplexCondition")
private val troubleshootingState =
combine(
@ -98,48 +89,63 @@ class SettingsViewModel(
}
}
val state: StateFlow<SettingsState?> =
combine(isLoading, troubleshootingState) { isLoading, troubleshootingState ->
SettingsState(
isLoading = isLoading,
debugMenu = troubleshootingState,
onBack = ::onBack,
items =
persistentListOf(
ZashiSettingsListItemState(
text = stringRes(R.string.settings_address_book),
icon = R.drawable.ic_settings_address_book,
onClick = ::onAddressBookClick
),
ZashiSettingsListItemState(
text = stringRes(R.string.settings_integrations),
icon = R.drawable.ic_settings_integrations,
onClick = ::onIntegrationsClick,
titleIcons =
listOfNotNull(
R.drawable.ic_integrations_coinbase,
R.drawable.ic_integrations_flexa.takeIf { isFlexaAvailable() }
).toImmutableList()
),
ZashiSettingsListItemState(
text = stringRes(R.string.settings_advanced_settings),
icon = R.drawable.ic_advanced_settings,
onClick = ::onAdvancedSettingsClick
),
ZashiSettingsListItemState(
text = stringRes(R.string.settings_about_us),
icon = R.drawable.ic_settings_info,
onClick = ::onAboutUsClick
),
ZashiSettingsListItemState(
text = stringRes(R.string.settings_feedback),
icon = R.drawable.ic_settings_feedback,
onClick = ::onSendUsFeedbackClick
),
),
version = stringRes(R.string.settings_version, versionInfo.versionName)
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), null)
val state: StateFlow<SettingsState> =
combine(
troubleshootingState,
isSensitiveSettingsVisible()
) { troubleshootingState, isSensitiveSettingsVisible ->
createState(troubleshootingState, isSensitiveSettingsVisible)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue =
createState(
troubleshootingState = null,
isSensitiveSettingsVisible = isSensitiveSettingsVisible().value
)
)
private fun createState(
troubleshootingState: SettingsTroubleshootingState?,
isSensitiveSettingsVisible: Boolean
) = SettingsState(
debugMenu = troubleshootingState,
onBack = ::onBack,
items =
listOfNotNull(
ZashiSettingsListItemState(
text = stringRes(R.string.settings_address_book),
icon = R.drawable.ic_settings_address_book,
onClick = ::onAddressBookClick
),
ZashiSettingsListItemState(
text = stringRes(R.string.settings_integrations),
icon = R.drawable.ic_settings_integrations,
onClick = ::onIntegrationsClick,
titleIcons =
listOfNotNull(
R.drawable.ic_integrations_coinbase,
R.drawable.ic_integrations_flexa.takeIf { isFlexaAvailable() }
).toImmutableList()
).takeIf { isSensitiveSettingsVisible },
ZashiSettingsListItemState(
text = stringRes(R.string.settings_advanced_settings),
icon = R.drawable.ic_advanced_settings,
onClick = ::onAdvancedSettingsClick
),
ZashiSettingsListItemState(
text = stringRes(R.string.settings_about_us),
icon = R.drawable.ic_settings_info,
onClick = ::onAboutUsClick
),
ZashiSettingsListItemState(
text = stringRes(R.string.settings_feedback),
icon = R.drawable.ic_settings_feedback,
onClick = ::onSendUsFeedbackClick
),
).toImmutableList(),
version = stringRes(R.string.settings_version, versionInfo.versionName)
)
val navigationCommand = MutableSharedFlow<String>()
val backNavigationCommand = MutableSharedFlow<Unit>()

View File

@ -10,9 +10,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.LocalActivity
import co.electriccoin.zcash.ui.common.compose.LocalNavController
import co.electriccoin.zcash.ui.common.viewmodel.CheckUpdateViewModel
import co.electriccoin.zcash.ui.navigateJustOnce
import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
import co.electriccoin.zcash.ui.screen.update.view.Update
@ -29,12 +32,19 @@ internal fun WrapCheckForUpdate() {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
val checkUpdateViewModel = koinActivityViewModel<CheckUpdateViewModel>()
// Check for an app update asynchronously. We create an effect that matches the activity
// lifecycle. If the wrapping compose recomposes, the check shouldn't run again.
LaunchedEffect(true) {
checkUpdateViewModel.checkForAppUpdate()
}
val activity = LocalActivity.current
val inputUpdateInfo = checkUpdateViewModel.updateInfo.collectAsStateWithLifecycle().value ?: return
val viewModel = koinActivityViewModel<UpdateViewModel> { parametersOf(inputUpdateInfo) }
val updateInfo = viewModel.updateInfo.collectAsStateWithLifecycle().value
val navController = LocalNavController.current
if (updateInfo.appUpdateInfo != null && updateInfo.state == UpdateState.Prepared) {
WrapUpdate(
@ -46,15 +56,12 @@ internal fun WrapCheckForUpdate() {
activity = activity,
appUpdateInfo = updateInfo.appUpdateInfo
)
},
onSettings = {
navController.navigateJustOnce(SETTINGS)
}
)
}
// Check for an app update asynchronously. We create an effect that matches the activity
// lifecycle. If the wrapping compose recomposes, the check shouldn't run again.
LaunchedEffect(true) {
checkUpdateViewModel.checkForAppUpdate()
}
}
@VisibleForTesting
@ -64,6 +71,7 @@ internal fun WrapUpdate(
checkForUpdate: () -> Unit,
remindLater: () -> Unit,
goForUpdate: () -> Unit,
onSettings: () -> Unit
) {
val activity = LocalActivity.current
@ -111,7 +119,8 @@ internal fun WrapUpdate(
snackbarHostState,
scope
)
}
},
onSettings = onSettings
)
}

View File

@ -19,12 +19,10 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flow
import kotlin.time.Duration.Companion.milliseconds
class AppUpdateCheckerMock private constructor() : AppUpdateChecker {
class AppUpdateCheckerMock : AppUpdateChecker {
companion object {
private const val DEFAULT_STALENESS_DAYS = 3
fun new() = AppUpdateCheckerMock()
// Used mostly for tests
val resultUpdateInfo =
UpdateInfoFixture.new(

View File

@ -6,5 +6,4 @@ package co.electriccoin.zcash.ui.screen.update
object UpdateTag {
const val BTN_LATER = "btn_later"
const val BTN_DOWNLOAD = "btn_download"
const val PROGRESSBAR_DOWNLOADING = "progressbar_downloading"
}

View File

@ -2,327 +2,197 @@ package co.electriccoin.zcash.ui.screen.update.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.Color
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.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.Header
import co.electriccoin.zcash.ui.design.component.Reference
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.component.ZashiButton
import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar
import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarCloseNavigation
import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarHamburgerNavigation
import co.electriccoin.zcash.ui.design.component.zashiVerticalGradient
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.util.scaffoldPadding
import co.electriccoin.zcash.ui.fixture.UpdateInfoFixture
import co.electriccoin.zcash.ui.screen.update.UpdateTag
import co.electriccoin.zcash.ui.screen.update.UpdateTag.BTN_DOWNLOAD
import co.electriccoin.zcash.ui.screen.update.UpdateTag.BTN_LATER
import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
@Preview
@Composable
private fun UpdatePreview() {
ZcashTheme(forceDarkMode = false) {
Update(
snackbarHostState = SnackbarHostState(),
UpdateInfoFixture.new(appUpdateInfo = null),
onDownload = {},
onLater = {},
onReference = {}
)
}
}
@Preview
@Composable
private fun UpdateRequiredPreview() {
ZcashTheme(forceDarkMode = false) {
Update(
snackbarHostState = SnackbarHostState(),
UpdateInfoFixture.new(force = true),
onDownload = {},
onLater = {},
onReference = {}
)
}
}
@Preview
@Composable
private fun UpdateAvailableDarkPreview() {
ZcashTheme(forceDarkMode = true) {
Update(
snackbarHostState = SnackbarHostState(),
UpdateInfoFixture.new(appUpdateInfo = null),
onDownload = {},
onLater = {},
onReference = {}
)
}
}
@Preview
@Composable
private fun UpdateRequiredDarkPreview() {
ZcashTheme(forceDarkMode = true) {
Update(
snackbarHostState = SnackbarHostState(),
UpdateInfoFixture.new(force = true),
onDownload = {},
onLater = {},
onReference = {}
)
}
}
@Composable
fun Update(
snackbarHostState: SnackbarHostState,
updateInfo: UpdateInfo,
onDownload: (state: UpdateState) -> Unit,
onLater: () -> Unit,
onReference: () -> Unit
onReference: () -> Unit,
onSettings: () -> Unit
) {
BlankBgScaffold(
topBar = {
UpdateTopAppBar(updateInfo = updateInfo)
},
snackbarHost = {
SnackbarHost(snackbarHostState)
},
bottomBar = {
UpdateBottomAppBar(
updateInfo,
onDownload,
onLater,
modifier = Modifier.fillMaxWidth()
)
}
) { paddingValues ->
UpdateContent(
onReference = onReference,
updateInfo = updateInfo,
modifier =
Modifier
.fillMaxWidth()
.scaffoldPadding(paddingValues)
)
}
UpdateOverlayRunning(updateInfo)
}
@Suppress("MagicNumber")
@Composable
fun UpdateOverlayRunning(updateInfo: UpdateInfo) {
if (updateInfo.state == UpdateState.Running) {
Column(
Modifier
.background(ZcashTheme.colors.overlay.copy(0.65f))
.fillMaxSize()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null // Set indication to null to disable ripple effect
) {}
.testTag(UpdateTag.PROGRESSBAR_DOWNLOADING),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
CircularProgressIndicator(color = ZcashTheme.colors.overlayProgressBar)
}
}
}
@Composable
private fun UpdateTopAppBar(updateInfo: UpdateInfo) {
SmallTopAppBar(
titleText =
stringResource(
updateInfo.isForce.let { force ->
if (force) {
R.string.update_critical_header
Box(
modifier =
Modifier.background(
zashiVerticalGradient(
if (updateInfo.isForce) {
ZashiColors.Utility.WarningYellow.utilityOrange100
} else {
R.string.update_header
ZashiColors.Utility.Purple.utilityPurple100
}
}
),
)
}
@Composable
@Suppress("LongMethod")
private fun UpdateBottomAppBar(
updateInfo: UpdateInfo,
onDownload: (state: UpdateState) -> Unit,
onLater: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
HorizontalDivider(
thickness = DividerDefaults.Thickness,
color = ZcashTheme.colors.primaryDividerColor
)
Column(
modifier =
Modifier
.padding(
top = ZashiDimensions.Spacing.spacingLg,
bottom = ZashiDimensions.Spacing.spacing3xl,
start = ZashiDimensions.Spacing.spacing3xl,
end = ZashiDimensions.Spacing.spacing3xl
),
horizontalAlignment = Alignment.CenterHorizontally
) {
ZashiButton(
onClick = { onDownload(UpdateState.Running) },
text = stringResource(R.string.update_download_button),
modifier =
Modifier
.testTag(UpdateTag.BTN_DOWNLOAD)
.fillMaxWidth(),
enabled = updateInfo.state != UpdateState.Running,
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
if (updateInfo.isForce) {
Text(
text = stringResource(R.string.update_later_disabled_button),
textAlign = TextAlign.Center,
style = ZcashTheme.typography.primary.bodyLarge,
fontWeight = FontWeight.SemiBold,
color = ZcashTheme.colors.textPrimary,
modifier =
Modifier
.padding(all = ZcashTheme.dimens.spacingDefault)
.testTag(UpdateTag.BTN_LATER)
)
} else {
Reference(
text = stringResource(R.string.update_later_enabled_button),
onClick = {
if (updateInfo.state != UpdateState.Running) {
onLater()
} else {
// Keep current state
)
) {
Scaffold(
topBar = {
ZashiSmallTopAppBar(
title = null,
subtitle = null,
colors = ZcashTheme.colors.topAppBarColors.copyColors(containerColor = Color.Transparent),
navigationAction = {
if (updateInfo.isForce.not()) {
ZashiTopAppBarCloseNavigation(modifier = Modifier.testTag(BTN_LATER), onBack = onLater)
}
},
hamburgerMenuActions = {
if (updateInfo.isForce) {
ZashiTopAppBarHamburgerNavigation(onSettings)
}
}
)
},
snackbarHost = {
SnackbarHost(snackbarHostState)
},
containerColor = Color.Transparent
) {
Column(modifier = Modifier.scaffoldPadding(it)) {
@Suppress("MagicNumber")
Spacer(Modifier.weight(.75f))
Image(
modifier = Modifier.align(Alignment.CenterHorizontally),
painter =
painterResource(
if (updateInfo.isForce) {
R.drawable.ic_update_required
} else {
R.drawable.ic_update
}
),
contentDescription = ""
)
Spacer(Modifier.height(24.dp))
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
modifier =
Modifier
.padding(all = ZcashTheme.dimens.spacingDefault)
.testTag(UpdateTag.BTN_LATER)
text =
if (updateInfo.isForce) {
stringResource(id = R.string.update_title_required)
} else {
stringResource(id = R.string.update_title_available)
},
style = ZashiTypography.header6,
fontWeight = FontWeight.SemiBold,
color = ZashiColors.Text.textPrimary
)
Spacer(Modifier.height(12.dp))
Text(
modifier = Modifier.fillMaxWidth(),
text =
buildAnnotatedString {
append(
if (updateInfo.isForce) {
stringResource(id = R.string.update_description_required)
} else {
stringResource(id = R.string.update_description_available)
}
)
appendLine()
appendLine()
withStyle(
style =
SpanStyle(
textDecoration = TextDecoration.Underline
)
) {
withLink(
LinkAnnotation.Clickable(CLICKABLE_TAG) {
if (updateInfo.state != UpdateState.Running) {
onReference()
}
}
) {
append(stringResource(R.string.update_link_text))
}
}
},
style = ZashiTypography.textSm,
textAlign = TextAlign.Center,
color = ZashiColors.Text.textPrimary,
)
Spacer(Modifier.weight(1f))
ZashiButton(
modifier = Modifier.fillMaxWidth().testTag(BTN_DOWNLOAD),
text = stringResource(R.string.update_download_button),
onClick = { onDownload(UpdateState.Running) },
enabled = updateInfo.state != UpdateState.Running,
isLoading = updateInfo.state == UpdateState.Running
)
}
}
}
}
@PreviewScreens
@Composable
@Suppress("LongMethod")
private fun UpdateContent(
onReference: () -> Unit,
updateInfo: UpdateInfo,
modifier: Modifier = Modifier,
) {
val appName = stringResource(id = R.string.app_name)
Column(
modifier =
modifier.then(
Modifier
.fillMaxHeight()
.verticalScroll(
rememberScrollState()
)
),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
imageVector =
if (updateInfo.isForce) {
ImageVector.vectorResource(R.drawable.ic_zashi_logo_sign_warn)
} else {
ImageVector.vectorResource(R.drawable.ic_zashi_logo_update_available)
},
colorFilter = ColorFilter.tint(color = ZcashTheme.colors.secondaryColor),
contentDescription = null
private fun UpdatePreview() =
ZcashTheme {
Update(
snackbarHostState = SnackbarHostState(),
updateInfo = UpdateInfoFixture.new(appUpdateInfo = null),
onDownload = {},
onLater = {},
onReference = {},
onSettings = {}
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingBig))
Header(
text =
if (updateInfo.isForce) {
stringResource(id = R.string.update_title_required)
} else {
stringResource(id = R.string.update_title_available, appName)
},
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
Body(
text =
if (updateInfo.isForce) {
stringResource(id = R.string.update_description_required, appName)
} else {
stringResource(id = R.string.update_description_available, appName)
},
textAlign = TextAlign.Center,
color = ZcashTheme.colors.textDescriptionDark
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
Reference(
text = stringResource(id = R.string.update_link_text),
onClick = {
if (updateInfo.state != UpdateState.Running) {
onReference()
} else {
// Keep current state
}
},
fontWeight = FontWeight.Normal,
textStyle = ZcashTheme.typography.primary.bodyMedium,
textAlign = TextAlign.Center,
color = ZcashTheme.colors.textDescriptionDark,
modifier = Modifier.padding(all = ZcashTheme.dimens.spacingDefault)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
}
}
@PreviewScreens
@Composable
private fun UpdateRequiredPreview() =
ZcashTheme {
Update(
snackbarHostState = SnackbarHostState(),
updateInfo = UpdateInfoFixture.new(force = true),
onDownload = {},
onLater = {},
onReference = {},
onSettings = {}
)
}
private const val CLICKABLE_TAG = "clickable"

View File

@ -12,8 +12,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.warning.view.NotEnoughSpaceView
import co.electriccoin.zcash.ui.screen.warning.viewmodel.StorageCheckViewModel
import co.electriccoin.zcash.ui.util.SettingsUtil
@ -24,12 +22,8 @@ fun MainActivity.WrapNotEnoughSpace(
goPrevious: () -> Unit,
goSettings: () -> Unit
) {
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val storageCheckViewModel = koinActivityViewModel<StorageCheckViewModel>()
val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value
val isEnoughFreeSpace = storageCheckViewModel.isEnoughSpace.collectAsStateWithLifecycle().value
if (isEnoughFreeSpace == true) {
goPrevious()
@ -46,7 +40,6 @@ fun MainActivity.WrapNotEnoughSpace(
goSettings = goSettings,
spaceAvailableMegabytes = spaceAvailableMegabytes.value ?: 0,
requiredStorageSpaceGigabytes = requiredStorageSpaceGigabytes,
walletState = walletState,
)
}
@ -55,7 +48,6 @@ private fun WrapNotEnoughFreeSpace(
goSettings: () -> Unit,
requiredStorageSpaceGigabytes: Int,
spaceAvailableMegabytes: Int,
walletState: TopAppBarSubTitleState,
) {
val context = LocalContext.current
@ -81,6 +73,5 @@ private fun WrapNotEnoughFreeSpace(
snackbarHostState = snackbarHostState,
storageSpaceRequiredGigabytes = requiredStorageSpaceGigabytes,
spaceAvailableMegabytes = spaceAvailableMegabytes,
topAppBarSubTitleState = walletState,
)
}

View File

@ -1,189 +1,141 @@
package co.electriccoin.zcash.ui.screen.warning.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.common.test.CommonTag
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.Header
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.component.ZashiButton
import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar
import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarHamburgerNavigation
import co.electriccoin.zcash.ui.design.component.zashiVerticalGradient
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.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.util.scaffoldPadding
@Preview
@Composable
private fun NotEnoughSpacePreview() {
ZcashTheme(forceDarkMode = false) {
NotEnoughSpaceView(
onSettings = {},
onSystemSettings = {},
snackbarHostState = SnackbarHostState(),
spaceAvailableMegabytes = 300,
storageSpaceRequiredGigabytes = 1,
topAppBarSubTitleState = TopAppBarSubTitleState.None,
)
}
}
@Preview
@Composable
private fun NotEnoughSpaceDarkPreview() {
ZcashTheme(forceDarkMode = true) {
NotEnoughSpaceView(
onSettings = {},
onSystemSettings = {},
snackbarHostState = SnackbarHostState(),
spaceAvailableMegabytes = 300,
storageSpaceRequiredGigabytes = 1,
topAppBarSubTitleState = TopAppBarSubTitleState.None,
)
}
}
@Composable
@Suppress("LongParameterList")
fun NotEnoughSpaceView(
onSettings: () -> Unit,
onSystemSettings: () -> Unit,
spaceAvailableMegabytes: Int,
storageSpaceRequiredGigabytes: Int,
topAppBarSubTitleState: TopAppBarSubTitleState,
snackbarHostState: SnackbarHostState,
) {
BlankBgScaffold(
topBar = {
NotEnoughSpaceTopAppBar(
onSettings = onSettings,
subTitleState = topAppBarSubTitleState,
Box(
modifier =
Modifier.background(
zashiVerticalGradient(ZashiColors.Utility.ErrorRed.utilityError100)
)
},
snackbarHost = { SnackbarHost(snackbarHostState) },
) { paddingValues ->
NotEnoughSpaceMainContent(
onSystemSettings = onSystemSettings,
spaceRequiredToContinueMegabytes = spaceAvailableMegabytes,
storageSpaceRequiredGigabytes = storageSpaceRequiredGigabytes,
modifier =
Modifier
.scaffoldPadding(paddingValues)
)
}
}
@Composable
private fun NotEnoughSpaceTopAppBar(
onSettings: () -> Unit,
subTitleState: TopAppBarSubTitleState
) {
SmallTopAppBar(
subTitle =
when (subTitleState) {
TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label)
TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label)
TopAppBarSubTitleState.None -> null
) {
Scaffold(
topBar = {
ZashiSmallTopAppBar(
colors = ZcashTheme.colors.topAppBarColors.copyColors(containerColor = Color.Transparent),
title = null,
subtitle = null,
hamburgerMenuActions = {
ZashiTopAppBarHamburgerNavigation(onSettings)
}
)
},
titleText = stringResource(id = R.string.not_enough_space_title).uppercase(),
hamburgerMenuActions = {
IconButton(
onClick = onSettings,
modifier = Modifier.testTag(CommonTag.SETTINGS_TOP_BAR_BUTTON)
) {
snackbarHost = {
SnackbarHost(snackbarHostState)
},
containerColor = Color.Transparent
) {
Column(modifier = Modifier.scaffoldPadding(it)) {
@Suppress("MagicNumber")
Spacer(Modifier.weight(.75f))
Image(
painter = painterResource(id = co.electriccoin.zcash.ui.design.R.drawable.ic_hamburger_menu),
contentDescription = stringResource(id = R.string.settings_menu_content_description)
modifier = Modifier.align(Alignment.CenterHorizontally),
painter = painterResource(R.drawable.ic_not_enough_space),
contentDescription = ""
)
Spacer(Modifier.height(24.dp))
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
text = stringResource(id = R.string.not_enough_space_title),
style = ZashiTypography.header6,
fontWeight = FontWeight.SemiBold,
color = ZashiColors.Text.textPrimary
)
Spacer(Modifier.height(12.dp))
Text(
modifier = Modifier.fillMaxWidth(),
text =
buildAnnotatedString {
append(
stringResource(
R.string.not_enough_space_description_1,
storageSpaceRequiredGigabytes
) + " "
)
withStyle(
SpanStyle(
fontWeight = FontWeight.Bold
)
) {
append(
stringResource(R.string.not_enough_space_description_2, spaceAvailableMegabytes)
)
}
append(
stringResource(
R.string.not_enough_space_description_3,
storageSpaceRequiredGigabytes *
GB_TO_MEGABYTES - spaceAvailableMegabytes
)
)
},
style = ZashiTypography.textSm,
textAlign = TextAlign.Center,
color = ZashiColors.Text.textPrimary,
)
Spacer(Modifier.weight(1f))
ZashiButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.not_enough_space_system_settings_btn),
onClick = onSystemSettings,
)
}
}
)
}
@Composable
private fun NotEnoughSpaceMainContent(
onSystemSettings: () -> Unit,
spaceRequiredToContinueMegabytes: Int,
storageSpaceRequiredGigabytes: Int,
modifier: Modifier = Modifier
) {
Column(
modifier =
modifier.then(
Modifier
.fillMaxHeight()
.verticalScroll(
rememberScrollState()
)
),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingBig))
Image(
painter = painterResource(id = R.drawable.ic_zashi_logo_sign_warn),
colorFilter = ColorFilter.tint(color = ZcashTheme.colors.secondaryColor),
contentDescription = null,
)
Spacer(Modifier.height(ZcashTheme.dimens.spacingBig))
Header(
text =
stringResource(
id = R.string.not_enough_space_description_1,
stringResource(id = R.string.app_name),
storageSpaceRequiredGigabytes,
spaceRequiredToContinueMegabytes
),
textAlign = TextAlign.Center
)
Spacer(Modifier.height(ZcashTheme.dimens.spacingLarge))
Body(
text =
stringResource(
id = R.string.not_enough_space_description_2,
stringResource(id = R.string.app_name)
),
textAlign = TextAlign.Center
)
Spacer(
modifier =
Modifier
.fillMaxHeight()
.weight(MINIMAL_WEIGHT)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
ZashiButton(
modifier = Modifier.fillMaxWidth(),
onClick = onSystemSettings,
text = stringResource(R.string.not_enough_space_system_settings_btn),
)
Spacer(Modifier.height(ZcashTheme.dimens.spacingHuge))
}
}
@PreviewScreens
@Composable
private fun NotEnoughSpacePreview() =
ZcashTheme {
NotEnoughSpaceView(
onSettings = {},
onSystemSettings = {},
snackbarHostState = SnackbarHostState(),
spaceAvailableMegabytes = 300,
storageSpaceRequiredGigabytes = 1,
)
}
private const val GB_TO_MEGABYTES = 1024

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="65dp"
android:height="64dp"
android:viewportWidth="65"
android:viewportHeight="64">
<path
android:pathData="M0.5,32C0.5,14.327 14.827,0 32.5,0C50.173,0 64.5,14.327 64.5,32C64.5,49.673 50.173,64 32.5,64C14.827,64 0.5,49.673 0.5,32Z"
android:fillColor="#3E1C96"/>
<path
android:pathData="M21.833,37.656C20.225,36.58 19.167,34.747 19.167,32.667C19.167,29.542 21.555,26.975 24.606,26.692C25.23,22.896 28.527,20 32.5,20C36.473,20 39.769,22.896 40.394,26.692C43.444,26.975 45.833,29.542 45.833,32.667C45.833,34.747 44.775,36.58 43.167,37.656M27.167,38.667L32.5,44M32.5,44L37.833,38.667M32.5,44V32"
android:strokeLineJoin="round"
android:strokeWidth="2.66667"
android:fillColor="#00000000"
android:strokeColor="#BDB4FE"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="65dp"
android:height="64dp"
android:viewportWidth="65"
android:viewportHeight="64">
<path
android:pathData="M0.5,32C0.5,14.327 14.827,0 32.5,0C50.173,0 64.5,14.327 64.5,32C64.5,49.673 50.173,64 32.5,64C14.827,64 0.5,49.673 0.5,32Z"
android:fillColor="#772917"/>
<path
android:pathData="M32.5,28V33.334M32.5,38.667H32.513M30.653,21.189L19.687,40.131C19.079,41.182 18.774,41.707 18.819,42.139C18.858,42.515 19.056,42.856 19.361,43.079C19.712,43.334 20.319,43.334 21.533,43.334H43.466C44.68,43.334 45.287,43.334 45.638,43.079C45.944,42.856 46.14,42.515 46.18,42.139C46.225,41.707 45.921,41.182 45.312,40.131L34.346,21.189C33.74,20.142 33.437,19.619 33.041,19.443C32.696,19.29 32.303,19.29 31.958,19.443C31.562,19.619 31.259,20.142 30.653,21.189Z"
android:strokeLineJoin="round"
android:strokeWidth="2.66667"
android:fillColor="#00000000"
android:strokeColor="#F7B27A"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="65dp"
android:height="64dp"
android:viewportWidth="65"
android:viewportHeight="64">
<path
android:pathData="M0.5,32C0.5,14.327 14.827,0 32.5,0C50.173,0 64.5,14.327 64.5,32C64.5,49.673 50.173,64 32.5,64C14.827,64 0.5,49.673 0.5,32Z"
android:fillColor="#EBE9FE"/>
<path
android:pathData="M21.833,37.656C20.225,36.58 19.167,34.747 19.167,32.667C19.167,29.542 21.555,26.975 24.606,26.692C25.23,22.896 28.527,20 32.5,20C36.473,20 39.769,22.896 40.394,26.692C43.444,26.975 45.833,29.542 45.833,32.667C45.833,34.747 44.775,36.58 43.167,37.656M27.167,38.667L32.5,44M32.5,44L37.833,38.667M32.5,44V32"
android:strokeLineJoin="round"
android:strokeWidth="2.66667"
android:fillColor="#00000000"
android:strokeColor="#5925DC"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="65dp"
android:height="64dp"
android:viewportWidth="65"
android:viewportHeight="64">
<path
android:pathData="M0.5,32C0.5,14.327 14.827,0 32.5,0C50.173,0 64.5,14.327 64.5,32C64.5,49.673 50.173,64 32.5,64C14.827,64 0.5,49.673 0.5,32Z"
android:fillColor="#FDEAD7"/>
<path
android:pathData="M32.5,28V33.334M32.5,38.667H32.513M30.653,21.189L19.687,40.131C19.079,41.182 18.774,41.707 18.819,42.139C18.858,42.515 19.056,42.856 19.361,43.079C19.712,43.334 20.319,43.334 21.533,43.334H43.466C44.68,43.334 45.287,43.334 45.638,43.079C45.944,42.856 46.14,42.515 46.18,42.139C46.225,41.707 45.921,41.182 45.312,40.131L34.346,21.189C33.74,20.142 33.437,19.619 33.041,19.443C32.696,19.29 32.303,19.29 31.958,19.443C31.562,19.619 31.259,20.142 30.653,21.189Z"
android:strokeLineJoin="round"
android:strokeWidth="2.66667"
android:fillColor="#00000000"
android:strokeColor="#B93815"
android:strokeLineCap="round"/>
</vector>

View File

@ -1,17 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="update_header">Actualización disponible</string>
<string name="update_critical_header">Actualización requerida</string>
<string name="update_title_available"><xliff:g id="app_name" example="Zashi">%1$s</xliff:g> aquí.</string>
<string name="update_title_available">Zashi aquí.</string>
<string name="update_title_required">No eres tú, soy yo.</string>
<string name="update_description_required">
Hay una actualización requerida para <xliff:g id="app_name" example="Zashi">%1$s</xliff:g> que realiza importantes mejoras en el rendimiento y/o la seguridad.
Hay una actualización requerida para Zashi que realiza importantes mejoras en el rendimiento y/o la seguridad.
</string>
<string name="update_description_available">
Hay una nueva versión de <xliff:g id="app_name" example="Zashi">%1$s</xliff:g> que realiza actualizaciones menores para mejorar el rendimiento y/o la seguridad.\n\nPor favor, toma un momento para actualizar a la última versión.
Hay una nueva versión de Zashi que realiza actualizaciones menores para mejorar el rendimiento y/o la seguridad.\n\nPor favor, toma un momento para actualizar a la última versión.
</string>
<string name="update_link_text">Obtén más información sobre esta actualización aquí.</string>
<string name="update_download_button">Actualizar</string>
<string name="update_later_enabled_button">Recordarme más tarde</string>
<string name="update_later_disabled_button">(requerido)</string>
</resources>

View File

@ -1,19 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="update_header">Update available</string>
<string name="update_critical_header">Update required</string>
<string name="update_title_available"><xliff:g id="app_name" example="Zashi">%1$s</xliff:g> here.</string>
<string name="update_title_required">It\'s not you, it\'s me.</string>
<string name="update_title_available">Zashi here.</string>
<string name="update_title_required">It\'s not you, it\'s us.</string>
<string name="update_description_required">
There is a required update for <xliff:g id="app_name" example="Zashi">%1$s</xliff:g> that makes major
improvements to performance and/or security.
There is a required update\nfor Zashi that makes major improvements to\nperformance and/or security.
</string>
<string name="update_description_available">
There is a new version of <xliff:g id="app_name" example="Zashi">%1$s</xliff:g> that makes minor updates to
There is a new version of Zashi that makes minor updates to
improve performance and/or security.\n\nPlease take a moment to update to the latest version.
</string>
<string name="update_link_text">Learn more about this update here.</string>
<string name="update_download_button">Update</string>
<string name="update_later_enabled_button">Remind me later</string>
<string name="update_later_disabled_button">(required)</string>
</resources>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="65dp"
android:height="64dp"
android:viewportWidth="65"
android:viewportHeight="64">
<path
android:pathData="M0.5,32C0.5,14.327 14.827,0 32.5,0C50.173,0 64.5,14.327 64.5,32C64.5,49.673 50.173,64 32.5,64C14.827,64 0.5,49.673 0.5,32Z"
android:fillColor="#7A271A"/>
<path
android:pathData="M32.5,26.667V32M32.5,37.334H32.513M45.833,32C45.833,39.364 39.864,45.334 32.5,45.334C25.136,45.334 19.167,39.364 19.167,32C19.167,24.636 25.136,18.667 32.5,18.667C39.864,18.667 45.833,24.636 45.833,32Z"
android:strokeLineJoin="round"
android:strokeWidth="2.66667"
android:fillColor="#00000000"
android:strokeColor="#FDA29B"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="65dp"
android:height="64dp"
android:viewportWidth="65"
android:viewportHeight="64">
<path
android:pathData="M0.5,32C0.5,14.327 14.827,0 32.5,0C50.173,0 64.5,14.327 64.5,32C64.5,49.673 50.173,64 32.5,64C14.827,64 0.5,49.673 0.5,32Z"
android:fillColor="#FEE4E2"/>
<path
android:pathData="M32.5,26.667V32M32.5,37.334H32.513M45.833,32C45.833,39.364 39.864,45.334 32.5,45.334C25.136,45.334 19.167,39.364 19.167,32C19.167,24.636 25.136,18.667 32.5,18.667C39.864,18.667 45.833,24.636 45.833,32Z"
android:strokeLineJoin="round"
android:strokeWidth="2.66667"
android:fillColor="#00000000"
android:strokeColor="#B42318"
android:strokeLineCap="round"/>
</vector>

View File

@ -1,15 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="not_enough_space_title">No hay suficiente espacio libre</string>
<string name="not_enough_space_description_1">
<xliff:g id="app_name" example="Zashi">%1$s</xliff:g> requiere al menos
<xliff:g example="1" id="required_gigabytes">%2$d</xliff:g> GB de espacio para funcionar, pero solo hay
<xliff:g example="300" id="available_megabytes">%3$d</xliff:g> MB disponibles.
</string>
<string name="not_enough_space_description_2">
Ve a la configuración de tu dispositivo y libera más espacio si deseas usar la aplicación
<xliff:g id="app_name" example="Zashi">%1$s</xliff:g>.
</string>
<string name="not_enough_space_description_1">Zashi requires <xliff:g example="1" id="required_gigabytes">%1$d</xliff:g> GB of space to synchronize the Zcash blockchain but there is only</string>
<string name="not_enough_space_description_2"><xliff:g example="300" id="available_megabytes">%1$d</xliff:g> MB available</string>
<string name="not_enough_space_description_3">. Syncing will stay paused until more space is available.\n\n~<xliff:g example="300" id="required_megabytes">%1$d</xliff:g> MB of additional space required to continue</string>
<string name="not_enough_space_system_settings_btn">Configuración del sistema</string>
<string name="not_enough_space_settings_open_failed">No se pudo iniciar la aplicación de Configuración.</string>
</resources>

View File

@ -1,15 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="not_enough_space_title">Not enough free space</string>
<string name="not_enough_space_description_1">
<xliff:g id="app_name" example="Zashi">%1$s</xliff:g> requires at least
<xliff:g example="1" id="required_gigabytes">%2$d</xliff:g> GB of space to operate but there is only
<xliff:g example="300" id="available_megabytes">%3$d</xliff:g> MB available.
</string>
<string name="not_enough_space_description_2">
Go to your device settings and make more space available if you wish to use the
<xliff:g id="app_name" example="Zashi">%1$s</xliff:g> app.
</string>
<string name="not_enough_space_description_1">Zashi requires <xliff:g example="1" id="required_gigabytes">%1$d</xliff:g> GB of space to synchronize the Zcash blockchain but there is only</string>
<string name="not_enough_space_description_2"><xliff:g example="300" id="available_megabytes">%1$d</xliff:g> MB available</string>
<string name="not_enough_space_description_3">. Syncing will stay paused until more space is available.\n\n~<xliff:g example="300" id="required_megabytes">%1$d</xliff:g> MB of additional space required to continue</string>
<string name="not_enough_space_system_settings_btn">System settings</string>
<string name="not_enough_space_settings_open_failed">Unable to launch Settings app.</string>
</resources>