[#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
|
### Added
|
||||||
- Grid pattern background has been added to several screens
|
- 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
|
### Changed
|
||||||
- The color palette used across the app has been reworked to align with the updated design document
|
- 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.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.navigation.NavBackStackEntry
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.NavOptionsBuilder
|
import androidx.navigation.NavOptionsBuilder
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
import cash.z.ecc.android.sdk.model.ZecSend
|
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.MULTIPLE_SUBMISSION_CLEAR_FORM
|
||||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_AMOUNT
|
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_AMOUNT
|
||||||
import co.electriccoin.zcash.ui.NavigationArguments.SEND_CONFIRM_INITIAL_STAGE
|
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.authentication.WrapAuthentication
|
||||||
import co.electriccoin.zcash.ui.screen.chooseserver.WrapChooseServer
|
import co.electriccoin.zcash.ui.screen.chooseserver.WrapChooseServer
|
||||||
import co.electriccoin.zcash.ui.screen.deletewallet.WrapDeleteWallet
|
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.exportdata.WrapExportPrivateData
|
||||||
import co.electriccoin.zcash.ui.screen.home.WrapHome
|
import co.electriccoin.zcash.ui.screen.home.WrapHome
|
||||||
import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator
|
import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator
|
||||||
|
@ -88,42 +93,7 @@ internal fun MainActivity.Navigation() {
|
||||||
popExitTransition = { popExitTransition() }
|
popExitTransition = { popExitTransition() }
|
||||||
) {
|
) {
|
||||||
composable(HOME) { backStack ->
|
composable(HOME) { backStack ->
|
||||||
WrapHome(
|
NavigationHome(navController, backStack)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
composable(SETTINGS) {
|
composable(SETTINGS) {
|
||||||
WrapSettings(
|
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
|
@Composable
|
||||||
private fun MainActivity.ShowSystemAuthentication(
|
private fun MainActivity.ShowSystemAuthentication(
|
||||||
navHostController: NavHostController,
|
navHostController: NavHostController,
|
||||||
|
|
|
@ -220,11 +220,11 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||||
combine(
|
combine(
|
||||||
synchronizer.transactions,
|
synchronizer.transactions,
|
||||||
synchronizer.status,
|
synchronizer.status,
|
||||||
synchronizer.networkHeight.filterNotNull()
|
synchronizer.networkHeight
|
||||||
) {
|
) {
|
||||||
transactions: List<TransactionOverview>,
|
transactions: List<TransactionOverview>,
|
||||||
status: Synchronizer.Status,
|
status: Synchronizer.Status,
|
||||||
networkHeight: BlockHeight ->
|
networkHeight: BlockHeight? ->
|
||||||
val enhancedTransactions =
|
val enhancedTransactions =
|
||||||
transactions
|
transactions
|
||||||
.sortedByDescending {
|
.sortedByDescending {
|
||||||
|
|
|
@ -11,7 +11,13 @@ data class TransactionOverviewExt(
|
||||||
val recipientAddressType: AddressType?
|
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
|
// Non-null assertion operator is necessary here as the smart cast to is impossible because `minedHeight` and
|
||||||
// `expiryHeight` are declared in a different module
|
// `expiryHeight` are declared in a different module
|
||||||
return when {
|
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="support_email_address" />
|
||||||
<string name="restoring_wallet_label">[Restoring Your Wallet…]</string>
|
<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="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>
|
</resources>
|
||||||
|
|
Loading…
Reference in New Issue