[#1458] Detect unavailable service and show a dialog
* [#1458] Detect unavailable service and show a dialog - Closes #1458 - Changelog update * Provide unsorted trx list when disconnected - These changes also improve the app’s ability to use the SDK’s APIs to provide a transaction history list, even in a disconnected state. In such a case, the list is sorted by its original order. - Changelog update
This commit is contained in:
parent
75e90607d1
commit
d813c1a9da
|
@ -11,6 +11,9 @@ directly impact users rather than highlighting other key architectural updates.*
|
|||
|
||||
### Added
|
||||
- Grid pattern background has been added to several screens
|
||||
- A new disconnected dialog reminder has been added to inform users about possible server issues
|
||||
- The transaction history list will be displayed when the app has server connection issues. Such a list might have a
|
||||
slightly different order.
|
||||
|
||||
### Changed
|
||||
- The color palette used across the app has been reworked to align with the updated design document
|
||||
|
|
|
@ -5,13 +5,17 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavOptionsBuilder
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.NavigationArguments.MULTIPLE_SUBMISSION_CLEAR_FORM
|
||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_AMOUNT
|
||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_INITIAL_STAGE
|
||||
|
@ -43,6 +47,7 @@ import co.electriccoin.zcash.ui.screen.authentication.AuthenticationUseCase
|
|||
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.exportdata.WrapExportPrivateData
|
||||
import co.electriccoin.zcash.ui.screen.home.WrapHome
|
||||
import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator
|
||||
|
@ -88,42 +93,7 @@ internal fun MainActivity.Navigation() {
|
|||
popExitTransition = { popExitTransition() }
|
||||
) {
|
||||
composable(HOME) { backStack ->
|
||||
WrapHome(
|
||||
goBack = { finish() },
|
||||
goScan = { navController.navigateJustOnce(SCAN) },
|
||||
goSendConfirmation = { zecSend ->
|
||||
navController.currentBackStackEntry?.savedStateHandle?.let { handle ->
|
||||
fillInHandleForConfirmation(handle, zecSend, SendConfirmationStage.Confirmation)
|
||||
}
|
||||
navController.navigateJustOnce(SEND_CONFIRMATION)
|
||||
},
|
||||
goSettings = { navController.navigateJustOnce(SETTINGS) },
|
||||
goMultiTrxSubmissionFailure = {
|
||||
// Ultimately we could approach reworking the MultipleTrxFailure screen into a separate
|
||||
// navigation endpoint
|
||||
navController.currentBackStackEntry?.savedStateHandle?.let { handle ->
|
||||
fillInHandleForConfirmation(handle, null, SendConfirmationStage.MultipleTrxFailure)
|
||||
}
|
||||
navController.navigateJustOnce(SEND_CONFIRMATION)
|
||||
},
|
||||
sendArguments =
|
||||
SendArguments(
|
||||
recipientAddress =
|
||||
backStack.savedStateHandle.get<String>(SEND_SCAN_RECIPIENT_ADDRESS)?.let {
|
||||
Json.decodeFromString<SerializableAddress>(it).toRecipient()
|
||||
},
|
||||
clearForm = backStack.savedStateHandle.get<Boolean>(MULTIPLE_SUBMISSION_CLEAR_FORM) ?: false
|
||||
).also {
|
||||
// Remove Send screen arguments passed from the Scan or MultipleSubmissionFailure screens if
|
||||
// some exist after we use them
|
||||
backStack.savedStateHandle.remove<String>(SEND_SCAN_RECIPIENT_ADDRESS)
|
||||
backStack.savedStateHandle.remove<Boolean>(MULTIPLE_SUBMISSION_CLEAR_FORM)
|
||||
},
|
||||
)
|
||||
|
||||
if (ConfigurationEntries.IS_APP_UPDATE_CHECK_ENABLED.getValue(RemoteConfig.current)) {
|
||||
WrapCheckForUpdate()
|
||||
}
|
||||
NavigationHome(navController, backStack)
|
||||
}
|
||||
composable(SETTINGS) {
|
||||
WrapSettings(
|
||||
|
@ -280,6 +250,65 @@ internal fun MainActivity.Navigation() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the Home screens sub-navigation. We could consider creating a separate sub-navigation graph.
|
||||
*/
|
||||
@Composable
|
||||
private fun MainActivity.NavigationHome(
|
||||
navController: NavHostController,
|
||||
backStack: NavBackStackEntry
|
||||
) {
|
||||
WrapHome(
|
||||
goBack = { finish() },
|
||||
goScan = { navController.navigateJustOnce(SCAN) },
|
||||
goSendConfirmation = { zecSend ->
|
||||
navController.currentBackStackEntry?.savedStateHandle?.let { handle ->
|
||||
fillInHandleForConfirmation(handle, zecSend, SendConfirmationStage.Confirmation)
|
||||
}
|
||||
navController.navigateJustOnce(SEND_CONFIRMATION)
|
||||
},
|
||||
goSettings = { navController.navigateJustOnce(SETTINGS) },
|
||||
goMultiTrxSubmissionFailure = {
|
||||
// Ultimately we could approach reworking the MultipleTrxFailure screen into a separate
|
||||
// navigation endpoint
|
||||
navController.currentBackStackEntry?.savedStateHandle?.let { handle ->
|
||||
fillInHandleForConfirmation(handle, null, SendConfirmationStage.MultipleTrxFailure)
|
||||
}
|
||||
navController.navigateJustOnce(SEND_CONFIRMATION)
|
||||
},
|
||||
sendArguments =
|
||||
SendArguments(
|
||||
recipientAddress =
|
||||
backStack.savedStateHandle.get<String>(SEND_SCAN_RECIPIENT_ADDRESS)?.let {
|
||||
Json.decodeFromString<SerializableAddress>(it).toRecipient()
|
||||
},
|
||||
clearForm = backStack.savedStateHandle.get<Boolean>(MULTIPLE_SUBMISSION_CLEAR_FORM) ?: false
|
||||
).also {
|
||||
// Remove Send screen arguments passed from the Scan or MultipleSubmissionFailure screens if
|
||||
// some exist after we use them
|
||||
backStack.savedStateHandle.remove<String>(SEND_SCAN_RECIPIENT_ADDRESS)
|
||||
backStack.savedStateHandle.remove<Boolean>(MULTIPLE_SUBMISSION_CLEAR_FORM)
|
||||
},
|
||||
)
|
||||
|
||||
val sdkStatus = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value?.status
|
||||
|
||||
if (Synchronizer.Status.DISCONNECTED == sdkStatus) {
|
||||
Twig.info { "Disconnected state received from Synchronizer" }
|
||||
|
||||
WrapDisconnected(
|
||||
goChooseServer = {
|
||||
navController.navigateJustOnce(CHOOSE_SERVER)
|
||||
},
|
||||
onIgnore = {
|
||||
// Keep the current navigation location
|
||||
}
|
||||
)
|
||||
} else if (ConfigurationEntries.IS_APP_UPDATE_CHECK_ENABLED.getValue(RemoteConfig.current)) {
|
||||
WrapCheckForUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainActivity.ShowSystemAuthentication(
|
||||
navHostController: NavHostController,
|
||||
|
|
|
@ -220,11 +220,11 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||
combine(
|
||||
synchronizer.transactions,
|
||||
synchronizer.status,
|
||||
synchronizer.networkHeight.filterNotNull()
|
||||
synchronizer.networkHeight
|
||||
) {
|
||||
transactions: List<TransactionOverview>,
|
||||
status: Synchronizer.Status,
|
||||
networkHeight: BlockHeight ->
|
||||
networkHeight: BlockHeight? ->
|
||||
val enhancedTransactions =
|
||||
transactions
|
||||
.sortedByDescending {
|
||||
|
|
|
@ -11,7 +11,13 @@ data class TransactionOverviewExt(
|
|||
val recipientAddressType: AddressType?
|
||||
)
|
||||
|
||||
fun TransactionOverview.getSortHeight(networkHeight: BlockHeight): BlockHeight {
|
||||
/**
|
||||
* This extension provides the best height that can currently be offered.
|
||||
*
|
||||
* @return It returns a height for the transaction list sorting in this order:
|
||||
* [minedHeight] -> [expiryHeight] -> [networkHeight] -> null
|
||||
*/
|
||||
fun TransactionOverview.getSortHeight(networkHeight: BlockHeight?): BlockHeight? {
|
||||
// Non-null assertion operator is necessary here as the smart cast to is impossible because `minedHeight` and
|
||||
// `expiryHeight` are declared in a different module
|
||||
return when {
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
@file:Suppress("ktlint:standard:filename")
|
||||
|
||||
package co.electriccoin.zcash.ui.screen.disconnected
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import co.electriccoin.zcash.ui.MainActivity
|
||||
import co.electriccoin.zcash.ui.screen.disconnected.model.DisconnectedUiState
|
||||
import co.electriccoin.zcash.ui.screen.disconnected.view.ServerDisconnected
|
||||
|
||||
@Composable
|
||||
internal fun MainActivity.WrapDisconnected(
|
||||
goChooseServer: () -> Unit,
|
||||
onIgnore: () -> Unit,
|
||||
) {
|
||||
co.electriccoin.zcash.ui.screen.disconnected.WrapDisconnected(
|
||||
goChooseServer = goChooseServer,
|
||||
onIgnore = onIgnore,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WrapDisconnected(
|
||||
goChooseServer: () -> Unit,
|
||||
onIgnore: () -> Unit,
|
||||
) {
|
||||
val (disconnectedUi, setDisconnectedUi) =
|
||||
rememberSaveable(stateSaver = DisconnectedUiState.Saver) { mutableStateOf(DisconnectedUiState.Displayed) }
|
||||
|
||||
if (disconnectedUi == DisconnectedUiState.Displayed) {
|
||||
ServerDisconnected(
|
||||
onChooseServer = {
|
||||
setDisconnectedUi(DisconnectedUiState.Dismissed)
|
||||
goChooseServer()
|
||||
},
|
||||
onIgnore = {
|
||||
setDisconnectedUi(DisconnectedUiState.Dismissed)
|
||||
onIgnore()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package co.electriccoin.zcash.ui.screen.disconnected.model
|
||||
|
||||
import androidx.compose.runtime.saveable.mapSaver
|
||||
|
||||
internal sealed class DisconnectedUiState {
|
||||
data object Displayed : DisconnectedUiState()
|
||||
|
||||
data object Dismissed : DisconnectedUiState()
|
||||
|
||||
companion object {
|
||||
private const val TYPE_DISPLAYED = "displayed" // $NON-NLS
|
||||
private const val TYPE_DISMISSED = "dismissed" // $NON-NLS
|
||||
private const val KEY_TYPE = "type" // $NON-NLS
|
||||
|
||||
internal val Saver
|
||||
get() =
|
||||
run {
|
||||
mapSaver(
|
||||
save = { it.toSaverMap() },
|
||||
restore = {
|
||||
if (it.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
val sendStageString = (it[KEY_TYPE] as String)
|
||||
when (sendStageString) {
|
||||
TYPE_DISPLAYED -> Displayed
|
||||
TYPE_DISMISSED -> Dismissed
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun DisconnectedUiState.toSaverMap(): HashMap<String, String> {
|
||||
val saverMap = HashMap<String, String>()
|
||||
when (this) {
|
||||
Displayed -> saverMap[KEY_TYPE] = TYPE_DISPLAYED
|
||||
Dismissed -> saverMap[KEY_TYPE] = TYPE_DISMISSED
|
||||
}
|
||||
return saverMap
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package co.electriccoin.zcash.ui.screen.disconnected.view
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.design.component.AppAlertDialog
|
||||
import co.electriccoin.zcash.ui.design.component.BlankSurface
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
|
||||
@Preview("Server Disconnected")
|
||||
@Composable
|
||||
private fun PreviewServerDisconnected() {
|
||||
ZcashTheme(forceDarkMode = false) {
|
||||
BlankSurface {
|
||||
ServerDisconnected(
|
||||
onChooseServer = {},
|
||||
onIgnore = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ServerDisconnected(
|
||||
onChooseServer: () -> Unit,
|
||||
onIgnore: () -> Unit,
|
||||
) {
|
||||
ServerDisconnectedDialog(
|
||||
onChooseServer = onChooseServer,
|
||||
onIgnore = onIgnore,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ServerDisconnectedDialog(
|
||||
onChooseServer: () -> Unit,
|
||||
onIgnore: () -> Unit,
|
||||
) {
|
||||
AppAlertDialog(
|
||||
title = stringResource(id = R.string.server_disconnected_dialog_title),
|
||||
text = {
|
||||
Column(
|
||||
Modifier.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.server_disconnected_dialog_message))
|
||||
}
|
||||
},
|
||||
confirmButtonText = stringResource(id = R.string.server_disconnected_dialog_switch_btn),
|
||||
onConfirmButtonClick = onChooseServer,
|
||||
dismissButtonText = stringResource(id = R.string.server_disconnected_dialog_ignore_btn),
|
||||
onDismissButtonClick = onIgnore,
|
||||
)
|
||||
}
|
|
@ -8,4 +8,11 @@
|
|||
<string name="support_email_address" />
|
||||
<string name="restoring_wallet_label">[Restoring Your Wallet…]</string>
|
||||
<string name="unable_to_open_play_store">Unable to launch Google Play store app…</string>
|
||||
|
||||
<string name="server_disconnected_dialog_title">Caution</string>
|
||||
<string name="server_disconnected_dialog_message">The server you\'re presently connected to is experiencing
|
||||
difficulties. For better performance, navigate to Advanced Settings and choose a different server. Please
|
||||
also check your device connection.</string>
|
||||
<string name="server_disconnected_dialog_switch_btn">Switch Server</string>
|
||||
<string name="server_disconnected_dialog_ignore_btn">Ignore</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in New Issue