[#1270] Synchronizer error in the alert view

- Changelog updated
- SyncProgress reducer has been refactored to the latest TCA
- Progress label is tappable when an error occurs and shows an alert view with the details
This commit is contained in:
Lukas Korba 2024-05-16 08:55:00 +02:00
parent 6d8096c20e
commit 0ad73b905d
8 changed files with 108 additions and 53 deletions

View File

@ -9,6 +9,7 @@ directly impact users rather than highlighting other crucial architectural updat
### Added
- Expanded transaction lists all text memos.
- Biometric lock is used to protect Delete Zashi, Export Private Data and Send features.
- Tapping on the error message label in the sync progress shows an alert view with the details of the error.
## 1.1 build 6 (2024-05-09)

View File

@ -43,7 +43,7 @@ public struct BalanceBreakdownReducer: Reducer {
public var partialProposalErrorState: PartialProposalError.State
public var pendingTransactions: Zatoshi
public var shieldedBalance: Zatoshi
public var syncProgressState: SyncProgressReducer.State
public var syncProgressState: SyncProgress.State
public var transparentBalance: Zatoshi
public var walletBalancesState: WalletBalances.State
@ -65,7 +65,7 @@ public struct BalanceBreakdownReducer: Reducer {
partialProposalErrorState: PartialProposalError.State,
pendingTransactions: Zatoshi,
shieldedBalance: Zatoshi = .zero,
syncProgressState: SyncProgressReducer.State,
syncProgressState: SyncProgress.State,
transparentBalance: Zatoshi = .zero,
walletBalancesState: WalletBalances.State
) {
@ -96,7 +96,7 @@ public struct BalanceBreakdownReducer: Reducer {
case shieldFundsPartial([String], [String])
case shieldFundsSuccess
case synchronizerStateChanged(RedactableSynchronizerState)
case syncProgress(SyncProgressReducer.Action)
case syncProgress(SyncProgress.Action)
case updateBalances(AccountBalance?)
case updateDestination(BalanceBreakdownReducer.State.Destination?)
case updateHintBoxVisibility(Bool)
@ -116,7 +116,7 @@ public struct BalanceBreakdownReducer: Reducer {
public var body: some Reducer<State, Action> {
Scope(state: \.syncProgressState, action: /Action.syncProgress) {
SyncProgressReducer()
SyncProgress()
}
Scope(state: \.partialProposalErrorState, action: /Action.partialProposalError) {

View File

@ -26,7 +26,7 @@ public struct HomeReducer: Reducer {
public var isRestoringWallet = false
public var migratingDatabase = true
public var scanState: Scan.State
public var syncProgressState: SyncProgressReducer.State
public var syncProgressState: SyncProgress.State
public var walletConfig: WalletConfig
public var transactionListState: TransactionListReducer.State
public var walletBalancesState: WalletBalances.State
@ -36,7 +36,7 @@ public struct HomeReducer: Reducer {
isRestoringWallet: Bool = false,
migratingDatabase: Bool = true,
scanState: Scan.State,
syncProgressState: SyncProgressReducer.State,
syncProgressState: SyncProgress.State,
transactionListState: TransactionListReducer.State,
walletBalancesState: WalletBalances.State,
walletConfig: WalletConfig
@ -65,7 +65,7 @@ public struct HomeReducer: Reducer {
case showSynchronizerErrorAlert(ZcashError)
case synchronizerStateChanged(RedactableSynchronizerState)
case syncFailed(ZcashError)
case syncProgress(SyncProgressReducer.Action)
case syncProgress(SyncProgress.Action)
case updateTransactionList([TransactionState])
case transactionList(TransactionListReducer.Action)
case walletBalances(WalletBalances.Action)
@ -85,7 +85,7 @@ public struct HomeReducer: Reducer {
}
Scope(state: \.syncProgressState, action: /Action.syncProgress) {
SyncProgressReducer()
SyncProgress()
}
Scope(state: \.walletBalancesState, action: /Action.walletBalances) {

View File

@ -14,12 +14,14 @@ import Models
import SDKSynchronizer
import Utils
public typealias SyncProgressStore = Store<SyncProgressReducer.State, SyncProgressReducer.Action>
public struct SyncProgressReducer: Reducer {
@Reducer
public struct SyncProgress {
private let CancelId = UUID()
public struct State: Equatable {
@ObservableState
public struct State: Equatable {
@Presents public var alert: AlertState<Action>?
public var lastKnownErrorMessage: String?
public var lastKnownSyncPercentage: Float = 0
public var synchronizerStatusSnapshot: SyncStatusSnapshot
public var syncStatusMessage = ""
@ -38,10 +40,12 @@ public struct SyncProgressReducer: Reducer {
}
public init(
lastKnownErrorMessage: String? = nil,
lastKnownSyncPercentage: Float,
synchronizerStatusSnapshot: SyncStatusSnapshot,
syncStatusMessage: String = ""
) {
self.lastKnownErrorMessage = lastKnownErrorMessage
self.lastKnownSyncPercentage = lastKnownSyncPercentage
self.synchronizerStatusSnapshot = synchronizerStatusSnapshot
self.syncStatusMessage = syncStatusMessage
@ -49,6 +53,8 @@ public struct SyncProgressReducer: Reducer {
}
public enum Action: Equatable {
case alert(PresentationAction<Action>)
case errorMessageTapped
case onAppear
case onDisappear
case synchronizerStateChanged(RedactableSynchronizerState)
@ -74,6 +80,19 @@ public struct SyncProgressReducer: Reducer {
case .onDisappear:
return .cancel(id: CancelId)
case .alert(.presented(let action)):
return Effect.send(action)
case .alert(.dismiss):
state.alert = nil
return .none
case .errorMessageTapped:
if let errorMessage = state.lastKnownErrorMessage {
state.alert = AlertState.errorMessage(errorMessage)
}
return .none
case .synchronizerStateChanged(let latestState):
let snapshot = SyncStatusSnapshot.snapshotFor(state: latestState.data.syncStatus)
if snapshot.syncStatus != state.synchronizerStatusSnapshot.syncStatus {
@ -83,6 +102,8 @@ public struct SyncProgressReducer: Reducer {
state.lastKnownSyncPercentage = progress
}
state.lastKnownErrorMessage = nil
switch snapshot.syncStatus {
case .syncing:
state.syncStatusMessage = L10n.Balances.syncing
@ -90,6 +111,7 @@ public struct SyncProgressReducer: Reducer {
state.lastKnownSyncPercentage = 1
state.syncStatusMessage = L10n.Balances.synced
case .error, .unprepared:
state.lastKnownErrorMessage = snapshot.message
#if DEBUG
state.syncStatusMessage = snapshot.message
#else
@ -104,21 +126,14 @@ public struct SyncProgressReducer: Reducer {
}
}
// MARK: - Store
// MARK: Alerts
extension SyncProgressStore {
public static var initial = SyncProgressStore(
initialState: .initial
) {
SyncProgressReducer()
extension AlertState where Action == SyncProgress.Action {
public static func errorMessage(_ message: String) -> AlertState {
AlertState {
TextState(L10n.Sync.Alert.title)
} message: {
TextState(message)
}
}
}
// MARK: - Placeholders
extension SyncProgressReducer.State {
public static let initial = SyncProgressReducer.State(
lastKnownSyncPercentage: 0,
synchronizerStatusSnapshot: .initial
)
}

View File

@ -13,20 +13,21 @@ import UIComponents
import Models
public struct SyncProgressView: View {
var store: SyncProgressStore
@Perception.Bindable var store: StoreOf<SyncProgress>
public init(store: SyncProgressStore) {
public init(store: StoreOf<SyncProgress>) {
self.store = store
}
public var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
WithPerceptionTracking {
VStack(spacing: 5) {
if viewStore.isSyncing {
if store.isSyncing {
HStack {
Text(viewStore.syncStatusMessage)
Text(store.syncStatusMessage)
.font(.custom(FontFamily.Inter.regular.name, size: 10))
.foregroundColor(Asset.Colors.primary.color)
// Frame height 0 is expected value because we want SwiftUI to ignore it
// for the vertical placement computation.
ProgressView()
@ -34,21 +35,35 @@ public struct SyncProgressView: View {
.frame(width: 11, height: 0)
}
} else {
Text(viewStore.syncStatusMessage)
.multilineTextAlignment(.center)
.font(.custom(FontFamily.Inter.regular.name, size: 10))
.padding(.horizontal, 35)
if let errorMessage = store.lastKnownErrorMessage {
Button {
store.send(.errorMessageTapped)
} label: {
Text(store.syncStatusMessage)
.multilineTextAlignment(.center)
.font(.custom(FontFamily.Inter.regular.name, size: 10))
.padding(.horizontal, 35)
.foregroundColor(Asset.Colors.primary.color)
}
} else {
Text(store.syncStatusMessage)
.multilineTextAlignment(.center)
.font(.custom(FontFamily.Inter.regular.name, size: 10))
.padding(.horizontal, 35)
.foregroundColor(Asset.Colors.primary.color)
}
}
Text(String(format: "%0.1f%%", viewStore.syncingPercentage * 100))
Text(String(format: "%0.1f%%", store.syncingPercentage * 100))
.font(.custom(FontFamily.Inter.black.name, size: 10))
.foregroundColor(Asset.Colors.primary.color)
ProgressView(value: viewStore.syncingPercentage, total: 1.0)
ProgressView(value: store.syncingPercentage, total: 1.0)
.progressViewStyle(ZashiSyncingProgressStyle())
}
.onAppear { viewStore.send(.onAppear) }
.onDisappear { viewStore.send(.onDisappear) }
.onAppear { store.send(.onAppear) }
.onDisappear { store.send(.onDisappear) }
.alert($store.scope(state: \.alert, action: \.alert))
}
}
}
@ -56,15 +71,34 @@ public struct SyncProgressView: View {
#Preview {
SyncProgressView(
store:
SyncProgressStore(
StoreOf<SyncProgress>(
initialState: .init(
lastKnownSyncPercentage: Float(0.43),
synchronizerStatusSnapshot: SyncStatusSnapshot(.syncing(0.41)),
syncStatusMessage: "Syncing"
)
) {
SyncProgressReducer()
SyncProgress()
}
)
.background(.red)
}
// MARK: - Store
extension SyncProgress {
public static var initial = StoreOf<SyncProgress>(
initialState: .initial
) {
SyncProgress()
}
}
// MARK: - Placeholders
extension SyncProgress.State {
public static let initial = SyncProgress.State(
lastKnownSyncPercentage: 0,
synchronizerStatusSnapshot: .initial
)
}

View File

@ -587,6 +587,10 @@ public enum L10n {
}
}
public enum Sync {
public enum Alert {
/// Error
public static let title = L10n.tr("Localizable", "sync.alert.title", fallback: "Error")
}
public enum Message {
/// Error: %@
public static func error(_ p1: Any) -> String {

View File

@ -156,6 +156,7 @@ Sharing this private data is irrevocable — once you have shared this private d
"sync.message.error" = "Error: %@";
"sync.message.stopped" = "Stopped";
"sync.message.sync" = "%@%% Synced";
"sync.alert.title" = "Error";
// MARK: - Common & Shared
"general.back" = "Back";

View File

@ -17,12 +17,12 @@ import Generated
final class SyncProgressTests: XCTestCase {
func testSyncingData() async throws {
let store = TestStore(
initialState: SyncProgressReducer.State(
initialState: SyncProgress.State(
lastKnownSyncPercentage: 0.0,
synchronizerStatusSnapshot: .snapshotFor(state: .syncing(0.513))
)
) {
SyncProgressReducer()
SyncProgress()
}
XCTAssertTrue(store.state.isSyncing)
@ -31,12 +31,12 @@ final class SyncProgressTests: XCTestCase {
func testlastKnownSyncingPercentage_Zero() async throws {
let store = TestStore(
initialState: SyncProgressReducer.State(
initialState: SyncProgress.State(
lastKnownSyncPercentage: 0.0,
synchronizerStatusSnapshot: .placeholder
)
) {
SyncProgressReducer()
SyncProgress()
}
XCTAssertEqual(store.state.lastKnownSyncPercentage, 0)
@ -45,12 +45,12 @@ final class SyncProgressTests: XCTestCase {
func testlastKnownSyncingPercentage_MoreThanZero() async throws {
let store = TestStore(
initialState: SyncProgressReducer.State(
initialState: SyncProgress.State(
lastKnownSyncPercentage: 0.15,
synchronizerStatusSnapshot: .placeholder
)
) {
SyncProgressReducer()
SyncProgress()
}
XCTAssertEqual(store.state.lastKnownSyncPercentage, 0.15)
@ -59,12 +59,12 @@ final class SyncProgressTests: XCTestCase {
func testlastKnownSyncingPercentage_FromSyncedState() async throws {
let store = TestStore(
initialState: SyncProgressReducer.State(
initialState: SyncProgress.State(
lastKnownSyncPercentage: 0.15,
synchronizerStatusSnapshot: .snapshotFor(state: .syncing(0.513))
)
) {
SyncProgressReducer()
SyncProgress()
}
var syncState: SynchronizerState = .zero
@ -80,12 +80,12 @@ final class SyncProgressTests: XCTestCase {
func testlastKnownSyncingPercentage_FromSyncingState() async throws {
let store = TestStore(
initialState: SyncProgressReducer.State(
initialState: SyncProgress.State(
lastKnownSyncPercentage: 0.15,
synchronizerStatusSnapshot: .snapshotFor(state: .syncing(0.513))
)
) {
SyncProgressReducer()
SyncProgress()
}
var syncState: SynchronizerState = .zero