[#974] Restore of the wallet UI (#989)

The broadcasting technology done, the views can subscribe to the restoring wallet state dependency
The restoring wallet badge implemented + handling of different backgrounds underneath it
Progress of the sync implemented on the Account screen
SyncProgress feature implemented, this new component is used 2 times already in Zashi so it's been separated into its own module (used in balances screen and at home screen when restoring the wallet)
Unit tests fixed + implemented new ones for the restore wallet flag
This commit is contained in:
Lukas Korba 2024-01-09 19:36:42 +01:00 committed by GitHub
parent 1d60dd3275
commit b801ac72d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1078 additions and 342 deletions

View File

@ -9,6 +9,7 @@ directly impact users rather than highlighting other crucial architectural updat
### Added
- The exported logs also show the shielded balances (total & verified) for every finished sync metric.
- Synchronization in the background. When the iPhone is connected to the power and wifi, the background task will try to synchronize randomly between 3-4am.
- Restore of the wallet is now indiated in the UI throughout the application.
### Fixed
- The export buttons are disabled when exporting of the private data is in progress.

View File

@ -46,7 +46,9 @@ let package = Package(
.library(name: "SendFlow", targets: ["SendFlow"]),
.library(name: "Settings", targets: ["Settings"]),
.library(name: "SupportDataGenerator", targets: ["SupportDataGenerator"]),
.library(name: "SyncProgress", targets: ["SyncProgress"]),
.library(name: "ReadTransactionsStorage", targets: ["ReadTransactionsStorage"]),
.library(name: "RestoreWalletStorage", targets: ["RestoreWalletStorage"]),
.library(name: "Tabs", targets: ["Tabs"]),
.library(name: "TransactionList", targets: ["TransactionList"]),
.library(name: "UIComponents", targets: ["UIComponents"]),
@ -103,7 +105,9 @@ let package = Package(
"MnemonicClient",
"Models",
"NumberFormatter",
"RestoreWalletStorage",
"SDKSynchronizer",
"SyncProgress",
"UIComponents",
"Utils",
"WalletStorage",
@ -215,10 +219,12 @@ let package = Package(
"DiskSpaceChecker",
"Generated",
"Models",
"RestoreWalletStorage",
"ReviewRequest",
"Scan",
"Settings",
"SDKSynchronizer",
"SyncProgress",
"UIComponents",
"Utils",
"TransactionList",
@ -311,6 +317,7 @@ let package = Package(
"DatabaseFiles",
"Generated",
"Models",
"RestoreWalletStorage",
"UIComponents",
"Utils",
.product(name: "ComposableArchitecture", package: "swift-composable-architecture")
@ -333,6 +340,13 @@ let package = Package(
],
path: "Sources/Features/RecoveryPhraseDisplay"
),
.target(
name: "RestoreWalletStorage",
dependencies: [
.product(name: "ComposableArchitecture", package: "swift-composable-architecture")
],
path: "Sources/Dependencies/RestoreWalletStorage"
),
.target(
name: "ReviewRequest",
dependencies: [
@ -358,6 +372,7 @@ let package = Package(
"OnboardingFlow",
"ReadTransactionsStorage",
"RecoveryPhraseDisplay",
"RestoreWalletStorage",
"Sandbox",
"SDKSynchronizer",
"Tabs",
@ -463,6 +478,7 @@ let package = Package(
"Models",
"PrivateDataConsent",
"RecoveryPhraseDisplay",
"RestoreWalletStorage",
"SDKSynchronizer",
"SupportDataGenerator",
"UIComponents",
@ -481,6 +497,18 @@ let package = Package(
],
path: "Sources/Dependencies/SupportDataGenerator"
),
.target(
name: "SyncProgress",
dependencies: [
"Generated",
"Models",
"SDKSynchronizer",
"UIComponents",
"Utils",
.product(name: "ComposableArchitecture", package: "swift-composable-architecture")
],
path: "Sources/Features/SyncProgress"
),
.target(
name: "ReadTransactionsStorage",
dependencies: [
@ -496,8 +524,10 @@ let package = Package(
"BalanceBreakdown",
"Generated",
"Home",
"RestoreWalletStorage",
"SendFlow",
"Settings",
"UIComponents",
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "ZcashLightClientKit", package: "ZcashLightClientKit")
],

View File

@ -0,0 +1,22 @@
//
// RestoreWalletStorageInterface.swift
//
//
// Created by Lukáš Korba on 19.12.2023.
//
import Foundation
import ComposableArchitecture
import Combine
extension DependencyValues {
public var restoreWalletStorage: RestoreWalletStorageClient {
get { self[RestoreWalletStorageClient.self] }
set { self[RestoreWalletStorageClient.self] = newValue }
}
}
public struct RestoreWalletStorageClient {
public var value: @Sendable () async -> AsyncStream<Bool>
public var updateValue: @Sendable (Bool) async -> Void
}

View File

@ -0,0 +1,35 @@
//
// RestoreWalletStorageLiveKey.swift
//
//
// Created by Lukáš Korba on 19.12.2023.
//
import Foundation
import ComposableArchitecture
import Combine
extension RestoreWalletStorageClient: DependencyKey {
public static var liveValue: Self {
let storage = CurrentValueSubject<Bool, Never>(false)
return .init(
value: {
AsyncStream { continuation in
let cancellable = storage.sink {
continuation.yield($0)
}
continuation.onTermination = { _ in
cancellable.cancel()
}
}
},
updateValue: { storage.value = $0 }
)
}
}
extension AsyncStream<Bool> {
static let placeholder = AsyncStream { continuation in continuation.finish() }
}

View File

@ -0,0 +1,24 @@
//
// RestoreWalletStorageTestKey.swift
//
//
// Created by Lukáš Korba on 19.12.2023.
//
import ComposableArchitecture
import XCTestDynamicOverlay
import Combine
extension RestoreWalletStorageClient: TestDependencyKey {
public static let testValue = Self(
value: XCTUnimplemented("\(Self.self).value", placeholder: .placeholder),
updateValue: XCTUnimplemented("\(Self.self).updateValue")
)
}
extension RestoreWalletStorageClient {
public static let noOp = Self(
value: { .placeholder },
updateValue: { _ in }
)
}

View File

@ -16,6 +16,8 @@ import Generated
import WalletStorage
import SDKSynchronizer
import Models
import SyncProgress
import RestoreWalletStorage
public typealias BalanceBreakdownStore = Store<BalanceBreakdownReducer.State, BalanceBreakdownReducer.Action>
public typealias BalanceBreakdownViewStore = ViewStore<BalanceBreakdownReducer.State, BalanceBreakdownReducer.Action>
@ -28,12 +30,11 @@ public struct BalanceBreakdownReducer: Reducer {
@PresentationState public var alert: AlertState<Action>?
public var autoShieldingThreshold: Zatoshi
public var changePending: Zatoshi
public var isRestoringWallet = false
public var isShieldingFunds: Bool
public var lastKnownSyncPercentage: Float = 0
public var pendingTransactions: Zatoshi
public var shieldedBalance: Balance
public var synchronizerStatusSnapshot: SyncStatusSnapshot
public var syncStatusMessage = ""
public var syncProgressState: SyncProgressReducer.State
public var transparentBalance: Balance
public var totalBalance: Zatoshi {
@ -48,37 +49,23 @@ public struct BalanceBreakdownReducer: Reducer {
isShieldingFunds || !isShieldableBalanceAvailable
}
public var isSyncing: Bool {
synchronizerStatusSnapshot.syncStatus.isSyncing
}
public var syncingPercentage: Float {
if case .syncing(let progress) = synchronizerStatusSnapshot.syncStatus {
return progress * 0.999
}
return lastKnownSyncPercentage
}
public init(
autoShieldingThreshold: Zatoshi,
changePending: Zatoshi,
isRestoringWallet: Bool = false,
isShieldingFunds: Bool,
lastKnownSyncPercentage: Float = 0,
pendingTransactions: Zatoshi,
shieldedBalance: Balance,
synchronizerStatusSnapshot: SyncStatusSnapshot,
syncStatusMessage: String = "",
syncProgressState: SyncProgressReducer.State,
transparentBalance: Balance
) {
self.autoShieldingThreshold = autoShieldingThreshold
self.changePending = changePending
self.isRestoringWallet = isRestoringWallet
self.isShieldingFunds = isShieldingFunds
self.lastKnownSyncPercentage = lastKnownSyncPercentage
self.pendingTransactions = pendingTransactions
self.shieldedBalance = shieldedBalance
self.synchronizerStatusSnapshot = synchronizerStatusSnapshot
self.syncStatusMessage = syncStatusMessage
self.syncProgressState = syncProgressState
self.transparentBalance = transparentBalance
}
}
@ -87,16 +74,20 @@ public struct BalanceBreakdownReducer: Reducer {
case alert(PresentationAction<Action>)
case onAppear
case onDisappear
case restoreWalletTask
case restoreWalletValue(Bool)
case shieldFunds
case shieldFundsSuccess(TransactionState)
case shieldFundsFailure(ZcashError)
case synchronizerStateChanged(SynchronizerState)
case syncProgress(SyncProgressReducer.Action)
}
@Dependency(\.derivationTool) var derivationTool
@Dependency(\.mainQueue) var mainQueue
@Dependency(\.mnemonic) var mnemonic
@Dependency(\.numberFormatter) var numberFormatter
@Dependency(\.restoreWalletStorage) var restoreWalletStorage
@Dependency(\.sdkSynchronizer) var sdkSynchronizer
@Dependency(\.walletStorage) var walletStorage
@ -105,6 +96,10 @@ public struct BalanceBreakdownReducer: Reducer {
}
public var body: some Reducer<State, Action> {
Scope(state: \.syncProgressState, action: /Action.syncProgress) {
SyncProgressReducer()
}
Reduce { state, action in
switch action {
case .alert(.presented(let action)):
@ -121,13 +116,24 @@ public struct BalanceBreakdownReducer: Reducer {
return .publisher {
sdkSynchronizer.stateStream()
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
.map(BalanceBreakdownReducer.Action.synchronizerStateChanged)
.map(Action.synchronizerStateChanged)
}
.cancellable(id: CancelId.timer, cancelInFlight: true)
case .onDisappear:
return .cancel(id: CancelId.timer)
case .restoreWalletTask:
return .run { send in
for await value in await restoreWalletStorage.value() {
await send(.restoreWalletValue(value))
}
}
case .restoreWalletValue(let value):
state.isRestoringWallet = value
return .none
case .shieldFunds:
state.isShieldingFunds = true
return .run { [state] send in
@ -157,29 +163,9 @@ public struct BalanceBreakdownReducer: Reducer {
case .synchronizerStateChanged(let latestState):
state.shieldedBalance = latestState.shieldedBalance.redacted
state.transparentBalance = latestState.transparentBalance.redacted
return .none
let snapshot = SyncStatusSnapshot.snapshotFor(state: latestState.syncStatus)
if snapshot.syncStatus != state.synchronizerStatusSnapshot.syncStatus {
state.synchronizerStatusSnapshot = snapshot
if case .syncing(let progress) = snapshot.syncStatus {
state.lastKnownSyncPercentage = progress
}
// TODO: [#931] The statuses of the sync process
// https://github.com/Electric-Coin-Company/zashi-ios/issues/931
// until then, this is temporary quick solution
switch snapshot.syncStatus {
case .syncing:
state.syncStatusMessage = L10n.Balances.syncing
case .upToDate:
state.lastKnownSyncPercentage = 1
state.syncStatusMessage = L10n.Balances.synced
case .error, .stopped, .unprepared:
state.syncStatusMessage = snapshot.message
}
}
case .syncProgress:
return .none
}
}
@ -207,7 +193,7 @@ extension BalanceBreakdownReducer.State {
isShieldingFunds: false,
pendingTransactions: .zero,
shieldedBalance: Balance.zero,
synchronizerStatusSnapshot: .placeholder,
syncProgressState: .initial,
transparentBalance: Balance.zero
)
@ -217,7 +203,7 @@ extension BalanceBreakdownReducer.State {
isShieldingFunds: false,
pendingTransactions: .zero,
shieldedBalance: Balance.zero,
synchronizerStatusSnapshot: .initial,
syncProgressState: .initial,
transparentBalance: Balance.zero
)
}

View File

@ -13,6 +13,7 @@ import UIComponents
import Utils
import Models
import BalanceFormatter
import SyncProgress
public struct BalanceBreakdownView: View {
let store: BalanceBreakdownStore
@ -24,30 +25,43 @@ public struct BalanceBreakdownView: View {
}
public var body: some View {
ZStack {
ScrollView {
WithViewStore(store, observe: { $0 }) { viewStore in
ScrollView {
BalanceWithIconView(balance: viewStore.shieldedBalance.data.total)
.padding(.top, 40)
.padding(.bottom, 5)
AvailableBalanceView(
balance: viewStore.shieldedBalance.data.verified,
tokenName: tokenName
)
Asset.Colors.primary.color
.frame(height: 1)
.padding(EdgeInsets(top: 30, leading: 30, bottom: 10, trailing: 30))
balancesBlock(viewStore)
transparentBlock(viewStore)
progressBlock(viewStore)
BalanceWithIconView(balance: viewStore.shieldedBalance.data.total)
.padding(.top, 40)
.padding(.bottom, 5)
.onAppear { viewStore.send(.onAppear) }
.onDisappear { viewStore.send(.onDisappear) }
AvailableBalanceView(
balance: viewStore.shieldedBalance.data.verified,
tokenName: tokenName
)
Asset.Colors.primary.color
.frame(height: 1)
.padding(EdgeInsets(top: 30, leading: 30, bottom: 10, trailing: 30))
balancesBlock(viewStore)
transparentBlock(viewStore)
if viewStore.isRestoringWallet {
Text(L10n.Balances.restoringWalletWarning)
.font(.custom(FontFamily.Inter.medium.name, size: 10))
.foregroundColor(Asset.Colors.error.color)
.multilineTextAlignment(.center)
.padding(.horizontal, 60)
.padding(.vertical, 20)
}
.onAppear { viewStore.send(.onAppear) }
.onDisappear { viewStore.send(.onDisappear) }
SyncProgressView(
store: store.scope(
state: \.syncProgressState,
action: BalanceBreakdownReducer.Action.syncProgress
)
)
.padding(.top, viewStore.isRestoringWallet ? 0 : 40)
}
}
.padding(.vertical, 1)
@ -56,6 +70,7 @@ public struct BalanceBreakdownView: View {
state: \.$alert,
action: { .alert($0) }
))
.task { await store.send(.restoreWalletTask).finish() }
}
}
@ -212,29 +227,6 @@ extension BalanceBreakdownView {
// }
}
@ViewBuilder func progressBlock(_ viewStore: BalanceBreakdownViewStore) -> some View {
VStack(spacing: 5) {
HStack {
Text(viewStore.syncStatusMessage)
.font(.custom(FontFamily.Inter.regular.name, size: 10))
if viewStore.isSyncing {
progressViewLooping()
}
}
.frame(height: 16)
.padding(.bottom, 5)
Text(String(format: "%0.1f%%", viewStore.syncingPercentage * 100))
.font(.custom(FontFamily.Inter.black.name, size: 10))
.foregroundColor(Asset.Colors.primary.color)
ProgressView(value: viewStore.syncingPercentage, total: 1.0)
.progressViewStyle(ZashiSyncingProgressStyle())
}
.padding(.top, 40)
}
@ViewBuilder func progressViewLooping() -> some View {
ProgressView()
.scaleEffect(0.7)
@ -254,8 +246,11 @@ extension BalanceBreakdownView {
isShieldingFunds: true,
pendingTransactions: .zero,
shieldedBalance: Balance(WalletBalance(verified: Zatoshi(25_234_778), total: Zatoshi(35_814_169))),
synchronizerStatusSnapshot: SyncStatusSnapshot(.syncing(0.41)),
syncStatusMessage: "Syncing",
syncProgressState: .init(
lastKnownSyncPercentage: 0.43,
synchronizerStatusSnapshot: SyncStatusSnapshot(.syncing(0.41)),
syncStatusMessage: "Syncing"
),
transparentBalance: Balance(WalletBalance(verified: Zatoshi(25_234_778), total: Zatoshi(35_814_169)))
)
) {

View File

@ -11,6 +11,8 @@ import Generated
import ReviewRequest
import TransactionList
import Scan
import SyncProgress
import RestoreWalletStorage
public typealias HomeStore = Store<HomeReducer.State, HomeReducer.Action>
public typealias HomeViewStore = ViewStore<HomeReducer.State, HomeReducer.Action>
@ -28,10 +30,12 @@ public struct HomeReducer: Reducer {
@PresentationState public var alert: AlertState<Action>?
public var destination: Destination?
public var canRequestReview = false
public var isRestoringWallet = false
public var requiredTransactionConfirmations = 0
public var scanState: ScanReducer.State
public var shieldedBalance: Balance
public var synchronizerStatusSnapshot: SyncStatusSnapshot
public var syncProgressState: SyncProgressReducer.State
public var walletConfig: WalletConfig
public var transactionListState: TransactionListReducer.State
public var migratingDatabase = true
@ -49,22 +53,26 @@ public struct HomeReducer: Reducer {
public init(
destination: Destination? = nil,
canRequestReview: Bool = false,
isRestoringWallet: Bool = false,
requiredTransactionConfirmations: Int = 0,
scanState: ScanReducer.State,
shieldedBalance: Balance,
synchronizerStatusSnapshot: SyncStatusSnapshot,
walletConfig: WalletConfig,
syncProgressState: SyncProgressReducer.State,
transactionListState: TransactionListReducer.State,
walletConfig: WalletConfig,
zecPrice: Decimal = Decimal(140.0)
) {
self.destination = destination
self.canRequestReview = canRequestReview
self.isRestoringWallet = isRestoringWallet
self.requiredTransactionConfirmations = requiredTransactionConfirmations
self.scanState = scanState
self.shieldedBalance = shieldedBalance
self.synchronizerStatusSnapshot = synchronizerStatusSnapshot
self.walletConfig = walletConfig
self.syncProgressState = syncProgressState
self.transactionListState = transactionListState
self.walletConfig = walletConfig
self.zecPrice = zecPrice
}
}
@ -77,11 +85,14 @@ public struct HomeReducer: Reducer {
case onAppear
case onDisappear
case resolveReviewRequest
case restoreWalletTask
case restoreWalletValue(Bool)
case retrySync
case reviewRequestFinished
case showSynchronizerErrorAlert(ZcashError)
case synchronizerStateChanged(SynchronizerState)
case syncFailed(ZcashError)
case syncProgress(SyncProgressReducer.Action)
case updateDestination(HomeReducer.State.Destination?)
case updateTransactionList([TransactionState])
case transactionList(TransactionListReducer.Action)
@ -90,6 +101,7 @@ public struct HomeReducer: Reducer {
@Dependency(\.audioServices) var audioServices
@Dependency(\.diskSpaceChecker) var diskSpaceChecker
@Dependency(\.mainQueue) var mainQueue
@Dependency(\.restoreWalletStorage) var restoreWalletStorage
@Dependency(\.reviewRequest) var reviewRequest
@Dependency(\.sdkSynchronizer) var sdkSynchronizer
@Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment
@ -103,6 +115,10 @@ public struct HomeReducer: Reducer {
TransactionListReducer()
}
Scope(state: \.syncProgressState, action: /Action.syncProgress) {
SyncProgressReducer()
}
Reduce { state, action in
switch action {
case .onAppear:
@ -148,6 +164,17 @@ public struct HomeReducer: Reducer {
}
return .none
case .restoreWalletTask:
return .run { send in
for await value in await restoreWalletStorage.value() {
await send(.restoreWalletValue(value))
}
}
case .restoreWalletValue(let value):
state.isRestoringWallet = value
return .none
case .reviewRequestFinished:
state.canRequestReview = false
return .none
@ -182,6 +209,9 @@ public struct HomeReducer: Reducer {
return .none
}
case .syncProgress:
return .none
case .foundTransactions:
return .run { _ in
reviewRequest.foundTransactions()
@ -261,8 +291,9 @@ extension HomeReducer.State {
scanState: .initial,
shieldedBalance: Balance.zero,
synchronizerStatusSnapshot: .initial,
walletConfig: .initial,
transactionListState: .initial
syncProgressState: .initial,
transactionListState: .initial,
walletConfig: .initial
)
}
}
@ -284,8 +315,9 @@ extension HomeStore {
synchronizerStatusSnapshot: .snapshotFor(
state: .error(ZcashError.synchronizerNotPrepared)
),
walletConfig: .initial,
transactionListState: .initial
syncProgressState: .initial,
transactionListState: .initial,
walletConfig: .initial
)
) {
HomeReducer(networkType: .testnet)

View File

@ -5,6 +5,9 @@ import Generated
import TransactionList
import Settings
import UIComponents
import SyncProgress
import Utils
import Models
public struct HomeView: View {
let store: HomeStore
@ -20,6 +23,18 @@ public struct HomeView: View {
VStack {
balance(viewStore)
if viewStore.isRestoringWallet {
SyncProgressView(
store: store.scope(
state: \.syncProgressState,
action: HomeReducer.Action.syncProgress
)
)
.frame(height: 94)
.frame(maxWidth: .infinity)
.background(Asset.Colors.shade92.color)
}
TransactionListView(store: store.historyStore(), tokenName: tokenName)
}
.applyScreenBackground()
@ -44,6 +59,7 @@ public struct HomeView: View {
destination: { NotEnoughFreeSpaceView(viewStore: viewStore) }
)
}
.task { await store.send(.restoreWalletTask).finish() }
}
}
@ -85,7 +101,34 @@ extension HomeView {
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
HomeView(store: .placeholder, tokenName: "ZEC")
HomeView(
store:
HomeStore(
initialState:
.init(
scanState: .initial,
shieldedBalance: Balance.zero,
synchronizerStatusSnapshot: .initial,
syncProgressState: .init(
lastKnownSyncPercentage: Float(0.43),
synchronizerStatusSnapshot: SyncStatusSnapshot(.syncing(0.41)),
syncStatusMessage: "Syncing"
),
transactionListState: .initial,
walletConfig: .initial
)
) {
HomeReducer(networkType: .testnet)
},
tokenName: "ZEC"
)
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(
trailing: Text("M")
)
.zashiTitle {
Text("Title")
}
}
}
}

View File

@ -15,6 +15,7 @@ import SwiftUI
import ExportLogs
import DatabaseFiles
import ExportLogs
import RestoreWalletStorage
public typealias PrivateDataConsentStore = Store<PrivateDataConsentReducer.State, PrivateDataConsentReducer.Action>
public typealias PrivateDataConsentViewStore = ViewStore<PrivateDataConsentReducer.State, PrivateDataConsentReducer.Action>
@ -23,11 +24,12 @@ public struct PrivateDataConsentReducer: Reducer {
let networkType: NetworkType
public struct State: Equatable {
@BindingState public var isAcknowledged: Bool = false
public var exportBinding: Bool
public var exportOnlyLogs = true
@BindingState public var isAcknowledged: Bool = false
public var isExportingData: Bool
public var isExportingLogs: Bool
public var isRestoringWallet = false
public var dataDbURL: [URL] = []
public var exportLogsState: ExportLogsReducer.State
@ -42,21 +44,23 @@ public struct PrivateDataConsentReducer: Reducer {
}
public init(
isAcknowledged: Bool = false,
dataDbURL: [URL],
exportBinding: Bool,
exportLogsState: ExportLogsReducer.State,
exportOnlyLogs: Bool = true,
isAcknowledged: Bool = false,
isExportingData: Bool = false,
isExportingLogs: Bool = false
isExportingLogs: Bool = false,
isRestoringWallet: Bool = false
) {
self.isAcknowledged = isAcknowledged
self.dataDbURL = dataDbURL
self.exportBinding = exportBinding
self.exportLogsState = exportLogsState
self.exportOnlyLogs = exportOnlyLogs
self.isAcknowledged = isAcknowledged
self.isExportingData = isExportingData
self.isExportingLogs = isExportingLogs
self.isRestoringWallet = isRestoringWallet
}
}
@ -66,6 +70,8 @@ public struct PrivateDataConsentReducer: Reducer {
case exportLogsRequested
case exportRequested
case onAppear
case restoreWalletTask
case restoreWalletValue(Bool)
case shareFinished
}
@ -74,6 +80,7 @@ public struct PrivateDataConsentReducer: Reducer {
}
@Dependency(\.databaseFiles) var databaseFiles
@Dependency(\.restoreWalletStorage) var restoreWalletStorage
public var body: some Reducer<State, Action> {
BindingReducer()
@ -86,7 +93,6 @@ public struct PrivateDataConsentReducer: Reducer {
switch action {
case .onAppear:
state.dataDbURL = [databaseFiles.dataDbURLFor(ZcashNetworkBuilder.network(for: networkType))]
state.isAcknowledged = false
return .none
case .exportLogs(.finished):
@ -106,6 +112,17 @@ public struct PrivateDataConsentReducer: Reducer {
state.exportOnlyLogs = false
return .send(.exportLogs(.start))
case .restoreWalletTask:
return .run { send in
for await value in await restoreWalletStorage.value() {
await send(.restoreWalletValue(value))
}
}
case .restoreWalletValue(let value):
state.isRestoringWallet = value
return .none
case .shareFinished:
state.isExportingData = false
state.isExportingLogs = false

View File

@ -23,6 +23,7 @@ public struct PrivateDataConsentView: View {
ScrollView {
Group {
ZashiIcon()
.padding(.top, viewStore.isRestoringWallet ? 30 : 0)
Text(L10n.PrivateDataConsent.title)
.font(.custom(FontFamily.Archivo.semiBold.name, size: 25))
@ -87,11 +88,13 @@ public struct PrivateDataConsentView: View {
.onAppear {
viewStore.send(.onAppear)
}
.restoringWalletBadge(isOn: viewStore.isRestoringWallet, background: .pattern)
shareLogsView(viewStore)
}
.navigationBarTitleDisplayMode(.inline)
.applyScreenBackground(withPattern: true)
.task { await store.send(.restoreWalletTask).finish() }
}
}

View File

@ -17,6 +17,7 @@ extension RootReducer {
public enum InitializationAction: Equatable {
case appDelegate(AppDelegateAction)
case checkBackupPhraseValidation
case checkRestoreWalletFlag(SyncStatus)
case checkWalletInitialization
case configureCrashReporter
case checkWalletConfig
@ -56,12 +57,12 @@ extension RootReducer {
}
case .synchronizerStateChanged(let latestState):
guard state.bgTask != nil else {
return .none
}
let snapshot = SyncStatusSnapshot.snapshotFor(state: latestState.syncStatus)
guard state.bgTask != nil else {
return .send(.initialization(.checkRestoreWalletFlag(snapshot.syncStatus)))
}
var finishBGTask = false
var successOfBGTask = false
@ -84,8 +85,18 @@ extension RootReducer {
return .cancel(id: CancelStateId.timer)
}
return .none
return .send(.initialization(.checkRestoreWalletFlag(snapshot.syncStatus)))
case .initialization(.checkRestoreWalletFlag(let syncStatus)):
if state.isRestoringWallet && syncStatus == .upToDate {
state.isRestoringWallet = false
return .run { _ in
await restoreWalletStorage.updateValue(false)
}
} else {
return .none
}
case .initialization(.synchronizerStartFailed):
return .none
@ -324,7 +335,13 @@ extension RootReducer {
return Effect.send(.destination(.updateDestination(.tabs)))
case .onboarding(.importWallet(.initializeSDK)):
return Effect.send(.initialization(.initializeSDK(.restoreWallet)))
state.isRestoringWallet = true
return .merge(
Effect.send(.initialization(.initializeSDK(.restoreWallet))),
.run { _ in
await restoreWalletStorage.updateValue(true)
}
)
case .initialization(.configureCrashReporter):
crashReporter.configure(

View File

@ -18,6 +18,7 @@ import CrashReporter
import ReadTransactionsStorage
import RecoveryPhraseDisplay
import BackgroundTasks
import RestoreWalletStorage
public typealias RootStore = Store<RootReducer.State, RootReducer.Action>
public typealias RootViewStore = ViewStore<RootReducer.State, RootReducer.Action>
@ -38,6 +39,7 @@ public struct RootReducer: Reducer {
public var debugState: DebugState
public var destinationState: DestinationState
public var exportLogsState: ExportLogsReducer.State
public var isRestoringWallet = false
public var onboardingState: OnboardingFlowReducer.State
public var phraseDisplayState: RecoveryPhraseDisplayReducer.State
public var sandboxState: SandboxReducer.State
@ -52,6 +54,7 @@ public struct RootReducer: Reducer {
debugState: DebugState,
destinationState: DestinationState,
exportLogsState: ExportLogsReducer.State,
isRestoringWallet: Bool = false,
onboardingState: OnboardingFlowReducer.State,
phraseDisplayState: RecoveryPhraseDisplayReducer.State,
sandboxState: SandboxReducer.State,
@ -64,6 +67,7 @@ public struct RootReducer: Reducer {
self.debugState = debugState
self.destinationState = destinationState
self.exportLogsState = exportLogsState
self.isRestoringWallet = isRestoringWallet
self.onboardingState = onboardingState
self.phraseDisplayState = phraseDisplayState
self.sandboxState = sandboxState
@ -114,6 +118,7 @@ public struct RootReducer: Reducer {
@Dependency(\.walletStorage) var walletStorage
@Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment
@Dependency(\.readTransactionsStorage) var readTransactionsStorage
@Dependency(\.restoreWalletStorage) var restoreWalletStorage
public init(tokenName: String, zcashNetwork: ZcashNetwork) {
self.tokenName = tokenName

View File

@ -12,6 +12,7 @@ import Generated
import WalletStorage
import SDKSynchronizer
import PrivateDataConsent
import RestoreWalletStorage
public typealias SettingsStore = Store<SettingsReducer.State, SettingsReducer.Action>
public typealias SettingsViewStore = ViewStore<SettingsReducer.State, SettingsReducer.Action>
@ -30,6 +31,7 @@ public struct SettingsReducer: Reducer {
public var appVersion = ""
public var appBuild = ""
public var destination: Destination?
public var isRestoringWallet = false
public var phraseDisplayState: RecoveryPhraseDisplayReducer.State
public var privateDataConsentState: PrivateDataConsentReducer.State
public var supportData: SupportData?
@ -38,6 +40,7 @@ public struct SettingsReducer: Reducer {
appVersion: String = "",
appBuild: String = "",
destination: Destination? = nil,
isRestoringWallet: Bool = false,
phraseDisplayState: RecoveryPhraseDisplayReducer.State,
privateDataConsentState: PrivateDataConsentReducer.State,
supportData: SupportData? = nil
@ -45,6 +48,7 @@ public struct SettingsReducer: Reducer {
self.appVersion = appVersion
self.appBuild = appBuild
self.destination = destination
self.isRestoringWallet = isRestoringWallet
self.phraseDisplayState = phraseDisplayState
self.privateDataConsentState = privateDataConsentState
self.supportData = supportData
@ -57,6 +61,8 @@ public struct SettingsReducer: Reducer {
case onAppear
case phraseDisplay(RecoveryPhraseDisplayReducer.Action)
case privateDataConsent(PrivateDataConsentReducer.Action)
case restoreWalletTask
case restoreWalletValue(Bool)
case sendSupportMail
case sendSupportMailFinished
case updateDestination(SettingsReducer.State.Destination?)
@ -67,6 +73,7 @@ public struct SettingsReducer: Reducer {
@Dependency(\.mnemonic) var mnemonic
@Dependency(\.sdkSynchronizer) var sdkSynchronizer
@Dependency(\.walletStorage) var walletStorage
@Dependency(\.restoreWalletStorage) var restoreWalletStorage
public init(networkType: NetworkType) {
self.networkType = networkType
@ -79,6 +86,7 @@ public struct SettingsReducer: Reducer {
state.appVersion = appVersion.appVersion()
state.appBuild = appVersion.appBuild()
return .none
case .backupWalletAccessRequest:
return .run { send in
if await localAuthentication.authenticate() {
@ -92,11 +100,27 @@ public struct SettingsReducer: Reducer {
case .phraseDisplay:
return .none
case .updateDestination(.privateDataConsent):
state.destination = .privateDataConsent
state.privateDataConsentState.isAcknowledged = false
return .none
case .updateDestination(let destination):
state.destination = destination
return .none
case .restoreWalletTask:
return .run { send in
for await value in await restoreWalletStorage.value() {
await send(.restoreWalletValue(value))
}
}
case .restoreWalletValue(let value):
state.isRestoringWallet = value
return .none
case .sendSupportMail:
if MFMailComposeViewController.canSendMail() {
state.supportData = SupportDataGenerator.generate()

View File

@ -7,6 +7,7 @@ import PrivateDataConsent
public struct SettingsView: View {
@Environment(\.openURL) var openURL
@State private var isRestoringWalletBadgeOn = false
let store: SettingsStore
@ -15,20 +16,55 @@ public struct SettingsView: View {
}
public var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
VStack {
ScrollView {
WithViewStore(store, observe: { $0 }) { viewStore in
Button(L10n.Settings.recoveryPhrase.uppercased()) {
viewStore.send(.backupWalletAccessRequest)
}
.zcashStyle()
.padding(.vertical, 25)
.navigationLinkEmpty(
isActive: viewStore.bindingForBackupPhrase,
destination: {
RecoveryPhraseDisplayView(store: store.backupPhraseStore())
}
)
.navigationLinkEmpty(
isActive: viewStore.bindingForAbout,
destination: {
About(store: store)
}
)
.navigationLinkEmpty(
isActive: viewStore.bindingForPrivateDataConsent,
destination: {
PrivateDataConsentView(store: store.privateDataConsentStore())
}
)
.onAppear {
viewStore.send(.onAppear)
isRestoringWalletBadgeOn = viewStore.isRestoringWallet
}
.onChange(of: viewStore.isRestoringWallet) { isRestoringWalletBadgeOn = $0 }
if let supportData = viewStore.supportData {
UIMailDialogView(
supportData: supportData,
completion: {
viewStore.send(.sendSupportMailFinished)
}
)
// UIMailDialogView only wraps MFMailComposeViewController presentation
// so frame is set to 0 to not break SwiftUIs layout
.frame(width: 0, height: 0)
}
Button(L10n.Settings.feedback.uppercased()) {
viewStore.send(.sendSupportMail)
}
.zcashStyle()
.padding(.bottom, 25)
Button(L10n.Settings.privacyPolicy.uppercased()) {
if let url = URL(string: "https://z.cash/privacy-policy/") {
openURL(url)
@ -36,61 +72,30 @@ public struct SettingsView: View {
}
.zcashStyle()
.padding(.bottom, 25)
Button(L10n.Settings.documentation.uppercased()) {
// TODO: - [#866] finish the documentation button action
// https://github.com/Electric-Coin-Company/zashi-ios/issues/866
}
.zcashStyle()
.padding(.bottom, 25)
Button(L10n.Settings.exportPrivateData.uppercased()) {
viewStore.send(.updateDestination(.privateDataConsent))
}
.zcashStyle()
.padding(.bottom, 80)
Spacer()
Button(L10n.Settings.about.uppercased()) {
viewStore.send(.updateDestination(.about))
}
.zcashStyle()
.padding(.bottom, 50)
}
.applyScreenBackground()
.navigationLinkEmpty(
isActive: viewStore.bindingForBackupPhrase,
destination: {
RecoveryPhraseDisplayView(store: store.backupPhraseStore())
}
)
.navigationLinkEmpty(
isActive: viewStore.bindingForAbout,
destination: {
About(store: store)
}
)
.navigationLinkEmpty(
isActive: viewStore.bindingForPrivateDataConsent,
destination: {
PrivateDataConsentView(store: store.privateDataConsentStore())
}
)
.onAppear { viewStore.send(.onAppear) }
if let supportData = viewStore.supportData {
UIMailDialogView(
supportData: supportData,
completion: {
viewStore.send(.sendSupportMailFinished)
}
)
// UIMailDialogView only wraps MFMailComposeViewController presentation
// so frame is set to 0 to not break SwiftUIs layout
.frame(width: 0, height: 0)
}
.padding(.horizontal, 70)
}
.padding(.horizontal, 70)
.padding(.vertical, 1)
.navigationBarTitleDisplayMode(.inline)
.applyScreenBackground()
.alert(store: store.scope(
state: \.$alert,
action: { .alert($0) }
@ -101,6 +106,8 @@ public struct SettingsView: View {
.resizable()
.frame(width: 62, height: 17)
}
.restoringWalletBadge(isOn: isRestoringWalletBadgeOn)
.task { await store.send(.restoreWalletTask).finish() }
}
}

View File

@ -54,9 +54,11 @@ public struct About: View {
.font(.custom(FontFamily.Archivo.bold.name, size: 14))
}
.padding(.horizontal, 70)
.restoringWalletBadge(isOn: viewStore.isRestoringWallet, background: .transparent)
}
.navigationBarTitleDisplayMode(.inline)
.applyScreenBackground(withPattern: true)
.task { await store.send(.restoreWalletTask).finish() }
}
}

View File

@ -0,0 +1,120 @@
//
// SyncProgressStore.swift
//
//
// Created by Lukáš Korba on 21.12.2023.
//
import Foundation
import ComposableArchitecture
import ZcashLightClientKit
import Generated
import Models
import SDKSynchronizer
public typealias SyncProgressStore = Store<SyncProgressReducer.State, SyncProgressReducer.Action>
public struct SyncProgressReducer: Reducer {
private enum CancelId { case timer }
public struct State: Equatable {
public var lastKnownSyncPercentage: Float = 0
public var synchronizerStatusSnapshot: SyncStatusSnapshot
public var syncStatusMessage = ""
public var isSyncing: Bool {
synchronizerStatusSnapshot.syncStatus.isSyncing
}
public var syncingPercentage: Float {
if case .syncing(let progress) = synchronizerStatusSnapshot.syncStatus {
return progress * 0.999
}
return lastKnownSyncPercentage
}
public init(
lastKnownSyncPercentage: Float,
synchronizerStatusSnapshot: SyncStatusSnapshot,
syncStatusMessage: String = ""
) {
self.lastKnownSyncPercentage = lastKnownSyncPercentage
self.synchronizerStatusSnapshot = synchronizerStatusSnapshot
self.syncStatusMessage = syncStatusMessage
}
}
public enum Action: Equatable {
case onAppear
case onDisappear
case synchronizerStateChanged(SynchronizerState)
}
@Dependency(\.mainQueue) var mainQueue
@Dependency(\.sdkSynchronizer) var sdkSynchronizer
public init() {}
public var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .onAppear:
return .publisher {
sdkSynchronizer.stateStream()
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
.map(Action.synchronizerStateChanged)
}
.cancellable(id: CancelId.timer, cancelInFlight: true)
case .onDisappear:
return .cancel(id: CancelId.timer)
case .synchronizerStateChanged(let latestState):
let snapshot = SyncStatusSnapshot.snapshotFor(state: latestState.syncStatus)
if snapshot.syncStatus != state.synchronizerStatusSnapshot.syncStatus {
state.synchronizerStatusSnapshot = snapshot
if case .syncing(let progress) = snapshot.syncStatus {
state.lastKnownSyncPercentage = progress
}
// TODO: [#931] The statuses of the sync process
// https://github.com/Electric-Coin-Company/zashi-ios/issues/931
// until then, this is temporary quick solution
switch snapshot.syncStatus {
case .syncing:
state.syncStatusMessage = L10n.Balances.syncing
case .upToDate:
state.lastKnownSyncPercentage = 1
state.syncStatusMessage = L10n.Balances.synced
case .error, .stopped, .unprepared:
state.syncStatusMessage = snapshot.message
}
}
return .none
}
}
}
}
// MARK: - Store
extension SyncProgressStore {
public static var initial = SyncProgressStore(
initialState: .initial
) {
SyncProgressReducer()
}
}
// MARK: - Placeholders
extension SyncProgressReducer.State {
public static let initial = SyncProgressReducer.State(
lastKnownSyncPercentage: 0,
synchronizerStatusSnapshot: .initial
)
}

View File

@ -0,0 +1,65 @@
//
// SyncProgressView.swift
//
//
// Created by Lukáš Korba on 21.12.2023.
//
import SwiftUI
import ComposableArchitecture
import Generated
import UIComponents
import Models
public struct SyncProgressView: View {
var store: SyncProgressStore
public init(store: SyncProgressStore) {
self.store = store
}
public var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
VStack(spacing: 5) {
HStack {
Text(viewStore.syncStatusMessage)
.font(.custom(FontFamily.Inter.regular.name, size: 10))
if viewStore.isSyncing {
ProgressView()
.scaleEffect(0.7)
.frame(width: 11, height: 14)
}
}
.frame(height: 16)
.padding(.bottom, 5)
Text(String(format: "%0.1f%%", viewStore.syncingPercentage * 100))
.font(.custom(FontFamily.Inter.black.name, size: 10))
.foregroundColor(Asset.Colors.primary.color)
ProgressView(value: viewStore.syncingPercentage, total: 1.0)
.progressViewStyle(ZashiSyncingProgressStyle())
}
.onAppear { viewStore.send(.onAppear) }
.onDisappear { viewStore.send(.onDisappear) }
}
}
}
#Preview {
SyncProgressView(
store:
SyncProgressStore(
initialState: .init(
lastKnownSyncPercentage: Float(0.43),
synchronizerStatusSnapshot: SyncStatusSnapshot(.syncing(0.41)),
syncStatusMessage: "Syncing"
)
) {
SyncProgressReducer()
}
)
.background(.red)
}

View File

@ -7,6 +7,8 @@
import Foundation
import ComposableArchitecture
import SwiftUI
import Generated
import AddressDetails
import BalanceBreakdown
@ -14,7 +16,7 @@ import Home
import SendFlow
import Settings
import ZcashLightClientKit
import SwiftUI
import RestoreWalletStorage
public typealias TabsStore = Store<TabsReducer.State, TabsReducer.Action>
public typealias TabsViewStore = ViewStore<TabsReducer.State, TabsReducer.Action>
@ -52,6 +54,7 @@ public struct TabsReducer: Reducer {
public var balanceBreakdownState: BalanceBreakdownReducer.State
public var destination: Destination?
public var homeState: HomeReducer.State
public var isRestoringWallet = false
public var selectedTab: Tab = .account
public var sendState: SendFlowReducer.State
public var settingsState: SettingsReducer.State
@ -61,6 +64,7 @@ public struct TabsReducer: Reducer {
balanceBreakdownState: BalanceBreakdownReducer.State,
destination: Destination? = nil,
homeState: HomeReducer.State,
isRestoringWallet: Bool = false,
selectedTab: Tab = .account,
sendState: SendFlowReducer.State,
settingsState: SettingsReducer.State
@ -69,6 +73,7 @@ public struct TabsReducer: Reducer {
self.balanceBreakdownState = balanceBreakdownState
self.destination = destination
self.homeState = homeState
self.isRestoringWallet = isRestoringWallet
self.selectedTab = selectedTab
self.sendState = sendState
self.settingsState = settingsState
@ -79,17 +84,21 @@ public struct TabsReducer: Reducer {
case addressDetails(AddressDetailsReducer.Action)
case balanceBreakdown(BalanceBreakdownReducer.Action)
case home(HomeReducer.Action)
case restoreWalletTask
case restoreWalletValue(Bool)
case selectedTabChanged(State.Tab)
case send(SendFlowReducer.Action)
case settings(SettingsReducer.Action)
case updateDestination(TabsReducer.State.Destination?)
}
@Dependency(\.restoreWalletStorage) var restoreWalletStorage
public init(tokenName: String, networkType: NetworkType) {
self.tokenName = tokenName
self.networkType = networkType
}
public var body: some Reducer<State, Action> {
Scope(state: \.sendState, action: /Action.send) {
SendFlowReducer(networkType: networkType)
@ -129,7 +138,18 @@ public struct TabsReducer: Reducer {
case .home:
return .none
case .restoreWalletTask:
return .run { send in
for await value in await restoreWalletStorage.value() {
await send(.restoreWalletValue(value))
}
}
case .restoreWalletValue(let value):
state.isRestoringWallet = value
return .none
case .send(.sendDone(let transaction)):
state.homeState.transactionListState.transactionList.insert(transaction, at: 0)
state.selectedTab = .account
@ -181,9 +201,7 @@ extension TabsViewStore {
func bindingForDestination(_ destination: TabsReducer.State.Destination) -> Binding<Bool> {
self.binding(
get: { $0.destination == destination },
send: { isActive in
return .updateDestination(isActive ? destination : nil)
}
send: { isActive in .updateDestination(isActive ? destination : nil) }
)
}
}

View File

@ -15,6 +15,7 @@ import BalanceBreakdown
import Home
import SendFlow
import Settings
import UIComponents
public struct TabsView: View {
let networkType: NetworkType
@ -30,7 +31,7 @@ public struct TabsView: View {
public var body: some View {
WithViewStore(self.store, observe: \.selectedTab) { tab in
WithViewStore(store, observe: { $0 }) { viewStore in
WithViewStore(self.store, observe: \.isRestoringWallet) { isRestoringWallet in
ZStack {
TabView(selection: tab.binding(send: TabsReducer.Action.selectedTabChanged)) {
HomeView(
@ -71,17 +72,17 @@ public struct TabsView: View {
}
.tabViewStyle(.page(indexDisplayMode: .never))
.padding(.bottom, 50)
VStack {
Spacer()
HStack {
ForEach((TabsReducer.State.Tab.allCases), id: \.self) { item in
Button {
viewStore.send(.selectedTabChanged(item), animation: .easeInOut)
store.send(.selectedTabChanged(item), animation: .easeInOut)
} label: {
VStack {
if viewStore.selectedTab == item {
if tab.state == item {//viewStore.selectedTab == item {
Text("\(item.title)")
.font(.custom(FontFamily.Archivo.black.name, size: 12))
.foregroundColor(Asset.Colors.primary.color)
@ -112,8 +113,10 @@ public struct TabsView: View {
.ignoresSafeArea(.keyboard)
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing: settingsButton(viewStore))
.navigationBarItems(trailing: settingsButton(store))
.zashiTitle { navBarView(tab.state) }
.restoringWalletBadge(isOn: isRestoringWallet.state)
.task { await store.send(.restoreWalletTask).finish() }
}
}
}
@ -135,18 +138,20 @@ public struct TabsView: View {
}
}
func settingsButton(_ viewStore: TabsViewStore) -> some View {
Image(systemName: "line.3.horizontal")
.resizable()
.frame(width: 21, height: 15)
.padding(15)
.navigationLink(
isActive: viewStore.bindingForDestination(.settings),
destination: {
SettingsView(store: store.settingsStore())
}
)
.tint(Asset.Colors.primary.color)
func settingsButton(_ store: TabsStore) -> some View {
WithViewStore(store, observe: { $0 }) { viewStore in
Image(systemName: "line.3.horizontal")
.resizable()
.frame(width: 21, height: 15)
.padding(15)
.navigationLink(
isActive: viewStore.bindingForDestination(.settings),
destination: {
SettingsView(store: store.settingsStore())
}
)
.tint(Asset.Colors.primary.color)
}
}
}

View File

@ -51,6 +51,8 @@ public enum L10n {
}
/// Pending transactions
public static let pendingTransactions = L10n.tr("Localizable", "balances.pendingTransactions", fallback: "Pending transactions")
/// The restore process can take several hours on lower-powered devices, and even on powerful devices is likely to take more than an hour.
public static let restoringWalletWarning = L10n.tr("Localizable", "balances.restoringWalletWarning", fallback: "The restore process can take several hours on lower-powered devices, and even on powerful devices is likely to take more than an hour.")
/// Shield and consolidate funds
public static let shieldButtonTitle = L10n.tr("Localizable", "balances.shieldButtonTitle", fallback: "Shield and consolidate funds")
/// Shielding funds
@ -133,6 +135,8 @@ public enum L10n {
public static let no = L10n.tr("Localizable", "general.no", fallback: "No")
/// Ok
public static let ok = L10n.tr("Localizable", "general.ok", fallback: "Ok")
/// [RESTORING YOUR WALLET]
public static let restoringWallet = L10n.tr("Localizable", "general.restoringWallet", fallback: "[RESTORING YOUR WALLET…]")
/// Send
public static let send = L10n.tr("Localizable", "general.send", fallback: "Send")
/// Skip

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 B

After

Width:  |  Height:  |  Size: 90 B

View File

@ -116,6 +116,7 @@
"balances.fee" = "(Fee %@)";
"balances.synced" = "Synced";
"balances.syncing" = "Syncing";
"balances.restoringWalletWarning" = "The restore process can take several hours on lower-powered devices, and even on powerful devices is likely to take more than an hour.";
"balances.alert.shieldFunds.failure.title" = "Failed to shield funds";
"balances.alert.shieldFunds.failure.message" = "Error: %@ (code: %@)";
@ -202,6 +203,7 @@ Sharing this private data is irrevocable — once you have shared this private d
"general.success" = "Success";
"general.unknown" = "Unknown";
"general.done" = "Done";
"general.restoringWallet" = "[RESTORING YOUR WALLET…]";
"balance.available" = "Available balance %@ %@";
"balance.availableTitle" = "Available Balance";
"qrCodeFor" = "QR Code for %@";

View File

@ -21,7 +21,6 @@ public struct ScreenBackgroundModifier: ViewModifier {
Asset.Assets.gridTile.image
.resizable(resizingMode: .tile)
.edgesIgnoringSafeArea(.all)
.opacity(0.18)
}
content

View File

@ -0,0 +1,88 @@
//
// RestoringWalletBadge.swift
//
//
// Created by Lukáš Korba on 18.12.2023.
//
import SwiftUI
import Generated
public struct RestoringWalletBadgeModifier: ViewModifier {
public enum Background {
case pattern
case solid
case transparent
}
let isOn: Bool
let background: Background
public func body(content: Content) -> some View {
if isOn {
ZStack(alignment: .top) {
content
.zIndex(0)
if background == .pattern {
RestoringWalletBadge()
.frame(maxWidth: .infinity)
.padding(.bottom, 6)
.background(
Asset.Assets.gridTile.image
.resizable(resizingMode: .tile)
)
.zIndex(1)
} else {
RestoringWalletBadge()
.frame(maxWidth: .infinity)
.padding(.bottom, 6)
.background(
background == .transparent
? .clear
: Asset.Colors.secondary.color
)
.zIndex(1)
}
}
} else {
content
}
}
}
extension View {
public func restoringWalletBadge(
isOn: Bool,
background: RestoringWalletBadgeModifier.Background = .solid
) -> some View {
modifier(
RestoringWalletBadgeModifier(isOn: isOn, background: background)
)
}
}
private struct RestoringWalletBadge: View {
var body: some View {
Text(L10n.General.restoringWallet)
.font(.custom(FontFamily.Archivo.semiBold.name, size: 12))
.foregroundStyle(Asset.Colors.shade55.color)
}
}
#Preview {
NavigationView {
ScrollView{
Text("Hello, World")
}
.padding(.vertical, 1)
.restoringWalletBadge(isOn: true)
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(
trailing: Text("M")
)
.zashiTitle {
Text("Title")
}
}
}

View File

@ -72,6 +72,7 @@
9E4938D82ACE8E8F003C4C1D /* SecurityWarningSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4938D72ACE8E8F003C4C1D /* SecurityWarningSnapshotTests.swift */; };
9E5AAEC02A67CEC4003F283D /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9E5AAEBF2A67CEC4003F283D /* Colors.xcassets */; };
9E5AAEC12A67CEC4003F283D /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9E5AAEBF2A67CEC4003F283D /* Colors.xcassets */; };
9E5B8E742B46E04E00CA3616 /* RestoreWalletTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5B8E732B46E04E00CA3616 /* RestoreWalletTests.swift */; };
9E6612362878345000C75B70 /* endlessCircleProgress.json in Resources */ = {isa = PBXBuildFile; fileRef = 9E6612352878345000C75B70 /* endlessCircleProgress.json */; };
9E683E472B0377F0002E7B5D /* WalletNukeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E683E462B0377F0002E7B5D /* WalletNukeTests.swift */; };
9E74CCD029DC0628003D6E32 /* ReviewRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E74CCCF29DC0628003D6E32 /* ReviewRequestTests.swift */; };
@ -83,6 +84,7 @@
9EB35D632A31F1DD00A2149B /* Root in Frameworks */ = {isa = PBXBuildFile; productRef = 9EB35D622A31F1DD00A2149B /* Root */; };
9EB35D6A2A3A2D7B00A2149B /* Utils in Frameworks */ = {isa = PBXBuildFile; productRef = 9EB35D692A3A2D7B00A2149B /* Utils */; };
9EB35D6C2A3A2D9200A2149B /* Utils in Frameworks */ = {isa = PBXBuildFile; productRef = 9EB35D6B2A3A2D9200A2149B /* Utils */; };
9EEB06C62B344F1E00EEE50F /* SyncProgressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEB06C52B344F1E00EEE50F /* SyncProgressTests.swift */; };
9EEB06C82B405A0400EEE50F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEB06C72B405A0400EEE50F /* AppDelegate.swift */; };
9EEB06C92B405A0400EEE50F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEB06C72B405A0400EEE50F /* AppDelegate.swift */; };
/* End PBXBuildFile section */
@ -144,6 +146,7 @@
9E4938D72ACE8E8F003C4C1D /* SecurityWarningSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityWarningSnapshotTests.swift; sourceTree = "<group>"; };
9E4A01762B0C9ABD005AFC7E /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
9E5AAEBF2A67CEC4003F283D /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = "<group>"; };
9E5B8E732B46E04E00CA3616 /* RestoreWalletTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreWalletTests.swift; sourceTree = "<group>"; };
9E5BF63E2819542C00BA3F17 /* TransactionListTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionListTests.swift; sourceTree = "<group>"; };
9E5BF643281FEC9900BA3F17 /* SendTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTests.swift; sourceTree = "<group>"; };
9E612C7829913F3600D09B09 /* SensitiveDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensitiveDataTests.swift; sourceTree = "<group>"; };
@ -170,6 +173,7 @@
9EDDEA9F2829610D00B4100C /* CurrencySelectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencySelectionTests.swift; sourceTree = "<group>"; };
9EDDEAA02829610D00B4100C /* TransactionAmountInputTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAmountInputTests.swift; sourceTree = "<group>"; };
9EDDEAA12829610D00B4100C /* TransactionAddressInputTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAddressInputTests.swift; sourceTree = "<group>"; };
9EEB06C52B344F1E00EEE50F /* SyncProgressTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncProgressTests.swift; sourceTree = "<group>"; };
9EEB06C72B405A0400EEE50F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
9EF8135A27ECC25E0075AF48 /* WalletStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletStorageTests.swift; sourceTree = "<group>"; };
9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorageTests.swift; sourceTree = "<group>"; };
@ -266,14 +270,13 @@
0D4E7A1926B364180058B01E /* secantTests */ = {
isa = PBXGroup;
children = (
9E139AAD2B07B39700D104B8 /* ZatoshiStringRepresentation */,
0D4E7A1C26B364180058B01E /* Info.plist */,
9E207C372966EF6E003E2C9B /* AddressDetailsTests */,
0DFE93DD272C6D4B000FCCA5 /* BackupFlowTests */,
9E94C61E28AA7DD5008256E9 /* BalanceBreakdownTests */,
9EAB4674285B5C68002904A0 /* DeeplinkTests */,
9E3911372848AD3A0073DD9A /* HomeTests */,
9E391122283E4C970073DD9A /* ImportWalletTests */,
0D4E7A1C26B364180058B01E /* Info.plist */,
9E6713EF2897F80A00A6796F /* MultiLineTextFieldTests */,
9E1FAFB82AF2C7DA0084CA3D /* PrivateDataConsentTests */,
9E74CCCE29DC060B003D6E32 /* ReviewRequestTests */,
@ -283,10 +286,12 @@
9E612C7729913F2300D09B09 /* SensitiveDataTests */,
9E66129C2889388C00C75B70 /* SettingsTests */,
9E391162284E3ECF0073DD9A /* SnapshotTests */,
9EEB06C42B344F0E00EEE50F /* SyncProgressTests */,
9E4691982AD573420082D7DF /* TabsTests */,
9E5BF63D281953F900BA3F17 /* TransactionListTests */,
9EF8135927ECC25E0075AF48 /* UtilTests */,
34F039B129ABCE8500CF0053 /* WalletConfigProviderTests */,
9E5BF63D281953F900BA3F17 /* TransactionListTests */,
9E139AAD2B07B39700D104B8 /* ZatoshiStringRepresentation */,
);
path = secantTests;
sourceTree = "<group>";
@ -585,10 +590,19 @@
9E391131284644580073DD9A /* AppInitializationTests.swift */,
9E852D6429B0A86300CF4AC1 /* DebugTests.swift */,
9E683E462B0377F0002E7B5D /* WalletNukeTests.swift */,
9E5B8E732B46E04E00CA3616 /* RestoreWalletTests.swift */,
);
path = RootTests;
sourceTree = "<group>";
};
9EEB06C42B344F0E00EEE50F /* SyncProgressTests */ = {
isa = PBXGroup;
children = (
9EEB06C52B344F1E00EEE50F /* SyncProgressTests.swift */,
);
path = SyncProgressTests;
sourceTree = "<group>";
};
9EF8135927ECC25E0075AF48 /* UtilTests */ = {
isa = PBXGroup;
children = (
@ -950,6 +964,7 @@
9E3451C429C857DF00177D16 /* View+UIImage.swift in Sources */,
9E34519729C4A51100177D16 /* RecoveryPhraseBackupTests.swift in Sources */,
9E0C0D702AFB842B00D69A16 /* TransactionStateTests.swift in Sources */,
9E5B8E742B46E04E00CA3616 /* RestoreWalletTests.swift in Sources */,
9E683E472B0377F0002E7B5D /* WalletNukeTests.swift in Sources */,
9E3451BC29C857C800177D16 /* NotEnoughFeeSpaceSnapshots.swift in Sources */,
9E3451B229C8565500177D16 /* SecItemClientTests.swift in Sources */,
@ -957,6 +972,7 @@
9E3451B329C8565500177D16 /* WalletBalance+testing.swift in Sources */,
9E3451AA29C84ED500177D16 /* CurrencySelectionTests.swift in Sources */,
9E3451C529C857E400177D16 /* TransactionListSnapshotTests.swift in Sources */,
9EEB06C62B344F1E00EEE50F /* SyncProgressTests.swift in Sources */,
9E34519829C4A51100177D16 /* RecoveryPhraseDisplayReducerTests.swift in Sources */,
9E34519C29C4A91A00177D16 /* HomeTests.swift in Sources */,
9E1FAFB72AF2C6D40084CA3D /* PrivateDataConsentSnapshotTests.swift in Sources */,
@ -1192,7 +1208,7 @@
CURRENT_PROJECT_VERSION = 11;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
DEVELOPMENT_TEAM = W5KABFU8SV;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = "secant/secant-testnet-Info.plist";
@ -1220,7 +1236,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
DEVELOPMENT_TEAM = W5KABFU8SV;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = "secant/secant-testnet-Info.plist";

View File

@ -88,7 +88,7 @@ class BalanceBreakdownTests: XCTestCase {
isShieldingFunds: false,
pendingTransactions: .zero,
shieldedBalance: Balance.zero,
synchronizerStatusSnapshot: .initial,
syncProgressState: .initial,
transparentBalance: Balance(
WalletBalance(
verified: Zatoshi(1_000_000),
@ -113,7 +113,7 @@ class BalanceBreakdownTests: XCTestCase {
isShieldingFunds: true,
pendingTransactions: .zero,
shieldedBalance: Balance.zero,
synchronizerStatusSnapshot: .initial,
syncProgressState: .initial,
transparentBalance: Balance(
WalletBalance(
verified: Zatoshi(1_000_000),
@ -130,130 +130,30 @@ class BalanceBreakdownTests: XCTestCase {
XCTAssertTrue(store.state.isShieldingButtonDisabled)
}
func testSyncingData() async throws {
let store = TestStore(
initialState: BalanceBreakdownReducer.State(
autoShieldingThreshold: Zatoshi(1_000_000),
changePending: .zero,
isShieldingFunds: true,
pendingTransactions: .zero,
shieldedBalance: Balance.zero,
synchronizerStatusSnapshot: .snapshotFor(state: .syncing(0.513)),
transparentBalance: Balance(
WalletBalance(
verified: Zatoshi(1_000_000),
total: Zatoshi(1_000_000)
)
)
)
) {
BalanceBreakdownReducer(networkType: .testnet)
}
XCTAssertTrue(store.state.isSyncing)
XCTAssertEqual(store.state.syncingPercentage, 0.513 * 0.999)
}
func testlastKnownSyncingPercentage_Zero() async throws {
let store = TestStore(
initialState: BalanceBreakdownReducer.State(
autoShieldingThreshold: Zatoshi(1_000_000),
changePending: .zero,
isShieldingFunds: true,
pendingTransactions: .zero,
shieldedBalance: Balance.zero,
synchronizerStatusSnapshot: .placeholder,
transparentBalance: Balance(
WalletBalance(
verified: Zatoshi(1_000_000),
total: Zatoshi(1_000_000)
)
)
)
) {
BalanceBreakdownReducer(networkType: .testnet)
}
XCTAssertEqual(store.state.lastKnownSyncPercentage, 0)
XCTAssertEqual(store.state.syncingPercentage, 0)
}
func testlastKnownSyncingPercentage_MoreThanZero() async throws {
let store = TestStore(
initialState: BalanceBreakdownReducer.State(
autoShieldingThreshold: Zatoshi(1_000_000),
changePending: .zero,
isShieldingFunds: true,
lastKnownSyncPercentage: 0.15,
pendingTransactions: .zero,
shieldedBalance: Balance.zero,
synchronizerStatusSnapshot: .placeholder,
transparentBalance: Balance(
WalletBalance(
verified: Zatoshi(1_000_000),
total: Zatoshi(1_000_000)
)
)
)
) {
BalanceBreakdownReducer(networkType: .testnet)
}
XCTAssertEqual(store.state.lastKnownSyncPercentage, 0.15)
XCTAssertEqual(store.state.syncingPercentage, 0.15)
}
func testlastKnownSyncingPercentage_FromSyncedState() async throws {
let store = TestStore(
initialState: BalanceBreakdownReducer.State(
autoShieldingThreshold: Zatoshi(1_000_000),
changePending: .zero,
isShieldingFunds: true,
lastKnownSyncPercentage: 0.15,
pendingTransactions: .zero,
shieldedBalance: Balance.zero,
synchronizerStatusSnapshot: .snapshotFor(state: .syncing(0.513)),
transparentBalance: .zero
)
) {
BalanceBreakdownReducer(networkType: .testnet)
}
var syncState: SynchronizerState = .zero
syncState.syncStatus = .upToDate
let snapshot = SyncStatusSnapshot.snapshotFor(state: syncState.syncStatus)
func testRestoreWalletSubscription() async throws {
var initialState = BalanceBreakdownReducer.State.initial
initialState.isRestoringWallet = false
await store.send(.synchronizerStateChanged(syncState)) { state in
state.synchronizerStatusSnapshot = snapshot
state.syncStatusMessage = "Synced"
state.lastKnownSyncPercentage = 1.0
}
}
func testlastKnownSyncingPercentage_FromSyncingState() async throws {
let store = TestStore(
initialState: BalanceBreakdownReducer.State(
autoShieldingThreshold: Zatoshi(1_000_000),
changePending: .zero,
isShieldingFunds: true,
lastKnownSyncPercentage: 0.15,
pendingTransactions: .zero,
shieldedBalance: Balance.zero,
synchronizerStatusSnapshot: .snapshotFor(state: .syncing(0.513)),
transparentBalance: .zero
)
initialState: initialState
) {
BalanceBreakdownReducer(networkType: .testnet)
}
var syncState: SynchronizerState = .zero
syncState.syncStatus = .syncing(0.545)
let snapshot = SyncStatusSnapshot.snapshotFor(state: syncState.syncStatus)
await store.send(.synchronizerStateChanged(syncState)) { state in
state.synchronizerStatusSnapshot = snapshot
state.syncStatusMessage = "Syncing"
state.lastKnownSyncPercentage = 0.545
store.dependencies.restoreWalletStorage = .noOp
store.dependencies.restoreWalletStorage.value = {
AsyncStream { continuation in
continuation.yield(true)
continuation.finish()
}
}
await store.send(.restoreWalletTask)
await store.receive(.restoreWalletValue(true)) { state in
state.isRestoringWallet = true
}
await store.finish()
}
}

View File

@ -26,8 +26,9 @@ class HomeTests: XCTestCase {
scanState: .initial,
shieldedBalance: Balance.zero,
synchronizerStatusSnapshot: mockSnapshot,
walletConfig: .initial,
transactionListState: .initial
syncProgressState: .initial,
transactionListState: .initial,
walletConfig: .initial
)
) {
HomeReducer(networkType: .testnet)
@ -115,4 +116,31 @@ class HomeTests: XCTestCase {
await store.finish()
}
@MainActor func testRestoreWalletSubscription() async throws {
var initialState = HomeReducer.State.initial
initialState.isRestoringWallet = false
let store = TestStore(
initialState: initialState
) {
HomeReducer(networkType: .testnet)
}
store.dependencies.restoreWalletStorage = .noOp
store.dependencies.restoreWalletStorage.value = {
AsyncStream { continuation in
continuation.yield(true)
continuation.finish()
}
}
await store.send(.restoreWalletTask)
await store.receive(.restoreWalletValue(true)) { state in
state.isRestoringWallet = true
}
await store.finish()
}
}

View File

@ -30,30 +30,6 @@ final class PrivateDataConsentTests: XCTestCase {
await store.finish()
}
func testClearOutAcknowledgeConfirmation() async throws {
let store = TestStore(
initialState: PrivateDataConsentReducer.State(
isAcknowledged: true,
dataDbURL: [],
exportBinding: false,
exportLogsState: .initial
)
) {
PrivateDataConsentReducer(networkType: .testnet)
}
let URL = URL(string: "https://electriccoin.co")!
store.dependencies.databaseFiles.dataDbURLFor = { _ in URL }
await store.send(.onAppear) { state in
state.dataDbURL = [URL]
state.isAcknowledged = false
}
await store.finish()
}
func testExportRequestSet() async throws {
let store = TestStore(
initialState: PrivateDataConsentReducer.State(
@ -139,6 +115,33 @@ final class PrivateDataConsentTests: XCTestCase {
await store.finish()
}
func testRestoreWalletSubscription() async throws {
var initialState = PrivateDataConsentReducer.State.initial
initialState.isRestoringWallet = false
let store = TestStore(
initialState: initialState
) {
PrivateDataConsentReducer(networkType: .testnet)
}
store.dependencies.restoreWalletStorage = .noOp
store.dependencies.restoreWalletStorage.value = {
AsyncStream { continuation in
continuation.yield(true)
continuation.finish()
}
}
await store.send(.restoreWalletTask)
await store.receive(.restoreWalletValue(true)) { state in
state.isRestoringWallet = true
}
await store.finish()
}
func testExportURLs_logsOnly() async throws {
let URLdb = URL(string: "http://db.url")!
let URLlogs = URL(string: "http://logs.url")!
@ -169,11 +172,11 @@ final class PrivateDataConsentTests: XCTestCase {
func testIsExportPossible_NoBecauseNotAcknowledged() async throws {
let state = PrivateDataConsentReducer.State(
isAcknowledged: false,
dataDbURL: [],
exportBinding: true,
exportLogsState: .initial,
exportOnlyLogs: true
exportOnlyLogs: true,
isAcknowledged: false
)
XCTAssertFalse(state.isExportPossible)
@ -181,11 +184,11 @@ final class PrivateDataConsentTests: XCTestCase {
func testIsExportPossible_NoBecauseExportingLogs() async throws {
let state = PrivateDataConsentReducer.State(
isAcknowledged: true,
dataDbURL: [],
exportBinding: true,
exportLogsState: .initial,
exportOnlyLogs: true,
isAcknowledged: true,
isExportingLogs: true
)
@ -194,11 +197,11 @@ final class PrivateDataConsentTests: XCTestCase {
func testIsExportPossible_NoBecauseExportingData() async throws {
let state = PrivateDataConsentReducer.State(
isAcknowledged: true,
dataDbURL: [],
exportBinding: true,
exportLogsState: .initial,
exportOnlyLogs: true,
isAcknowledged: true,
isExportingData: true
)
@ -207,11 +210,11 @@ final class PrivateDataConsentTests: XCTestCase {
func testIsExportPossible() async throws {
let state = PrivateDataConsentReducer.State(
isAcknowledged: true,
dataDbURL: [],
exportBinding: true,
exportLogsState: .initial,
exportOnlyLogs: true
exportOnlyLogs: true,
isAcknowledged: true
)
XCTAssertTrue(state.isExportPossible)

View File

@ -54,6 +54,7 @@ class AppInitializationTests: XCTestCase {
store.dependencies.sdkSynchronizer = .noOp
store.dependencies.crashReporter = .noOp
store.dependencies.numberFormatter = .noOp
store.dependencies.restoreWalletStorage = .noOp
// Root of the test, the app finished the launch process and triggers the checks and initializations.
await store.send(.initialization(.appDelegate(.didFinishLaunching)))
@ -127,6 +128,7 @@ class AppInitializationTests: XCTestCase {
store.dependencies.sdkSynchronizer = .noOp
store.dependencies.crashReporter = .noOp
store.dependencies.numberFormatter = .noOp
store.dependencies.restoreWalletStorage = .noOp
// Root of the test, the app finished the launch process and triggers the checks and initializations.
await store.send(.initialization(.appDelegate(.didFinishLaunching)))
@ -184,6 +186,7 @@ class AppInitializationTests: XCTestCase {
store.dependencies.mainQueue = .immediate
store.dependencies.walletConfigProvider = .noOp
store.dependencies.crashReporter = .noOp
store.dependencies.restoreWalletStorage = .noOp
// Root of the test, the app finished the launch process and triggers the checks and initializations.
await store.send(.initialization(.appDelegate(.didFinishLaunching)))
@ -215,6 +218,7 @@ class AppInitializationTests: XCTestCase {
store.dependencies.walletStorage = .noOp
store.dependencies.walletConfigProvider = .noOp
store.dependencies.crashReporter = .noOp
store.dependencies.restoreWalletStorage = .noOp
// Root of the test, the app finished the launch process and triggers the checks and initializations.
await store.send(.initialization(.appDelegate(.didFinishLaunching)))

View File

@ -0,0 +1,82 @@
//
// RestoreWalletTests.swift
// secantTests
//
// Created by Lukáš Korba on 04.01.2024.
//
import XCTest
import Combine
import ComposableArchitecture
import Root
import Utils
import ZcashLightClientKit
@testable import secant_testnet
@MainActor
final class RestoreWalletTests: XCTestCase {
func testIsRestoringWallet() async throws {
let store = TestStore(
initialState: .initial
) {
RootReducer(tokenName: "ZEC", zcashNetwork: ZcashNetworkBuilder.network(for: .testnet))
}
store.dependencies.mainQueue = .immediate
store.dependencies.mnemonic = .noOp
store.dependencies.restoreWalletStorage = .noOp
store.dependencies.restoreWalletStorage.updateValue = { value in
XCTAssertTrue(value)
}
store.dependencies.sdkSynchronizer = .noOp
store.dependencies.walletStorage = .noOp
await store.send(.onboarding(.importWallet(.initializeSDK))) { state in
state.isRestoringWallet = true
}
await store.receive(.initialization(.initializeSDK(.restoreWallet))) { state in
state.storedWallet = .placeholder
}
await store.receive(.initialization(.initializationSuccessfullyDone(nil)))
await store.receive(.initialization(.registerForSynchronizersUpdate))
await store.finish()
}
func testIsRestoringWalletFinished() async throws {
var state = RootReducer.State.initial
state.isRestoringWallet = true
let store = TestStore(
initialState: state
) {
RootReducer(
tokenName: "ZEC",
zcashNetwork: ZcashNetworkBuilder.network(for: .testnet)
)
}
store.dependencies.mainQueue = .immediate
store.dependencies.mnemonic = .noOp
store.dependencies.restoreWalletStorage = .noOp
store.dependencies.restoreWalletStorage.updateValue = { value in
XCTAssertFalse(value)
}
store.dependencies.sdkSynchronizer = .noOp
store.dependencies.walletStorage = .noOp
var syncState: SynchronizerState = .zero
syncState.syncStatus = .upToDate
await store.send(.synchronizerStateChanged(syncState))
await store.receive(.initialization(.checkRestoreWalletFlag(syncState.syncStatus))) { state in
state.isRestoringWallet = false
}
await store.finish()
}
}

View File

@ -85,4 +85,31 @@ class SettingsTests: XCTestCase {
await store.finish()
}
func testRestoreWalletSubscription() async throws {
var initialState = SettingsReducer.State.initial
initialState.isRestoringWallet = false
let store = TestStore(
initialState: initialState
) {
SettingsReducer(networkType: .testnet)
}
store.dependencies.restoreWalletStorage = .noOp
store.dependencies.restoreWalletStorage.value = {
AsyncStream { continuation in
continuation.yield(true)
continuation.finish()
}
}
await store.send(.restoreWalletTask)
await store.receive(.restoreWalletValue(true)) { state in
state.isRestoringWallet = true
}
await store.finish()
}
}

View File

@ -21,7 +21,7 @@ class BalanceBreakdownSnapshotTests: XCTestCase {
isShieldingFunds: false,
pendingTransactions: .zero,
shieldedBalance: WalletBalance(verified: Zatoshi(123_000_000_000), total: Zatoshi(123_000_000_000)).redacted,
synchronizerStatusSnapshot: .initial,
syncProgressState: .initial,
transparentBalance: WalletBalance(verified: Zatoshi(850_000_000), total: Zatoshi(850_000_000)).redacted
)
) {

View File

@ -42,8 +42,9 @@ class HomeSnapshotTests: XCTestCase {
scanState: .initial,
shieldedBalance: balance.redacted,
synchronizerStatusSnapshot: .initial,
walletConfig: .initial,
transactionListState: .init(transactionList: IdentifiedArrayOf(uniqueElements: transactionList))
syncProgressState: .initial,
transactionListState: .init(transactionList: IdentifiedArrayOf(uniqueElements: transactionList)),
walletConfig: .initial
)
) {
HomeReducer(networkType: .testnet)

View File

@ -67,7 +67,9 @@ class SendSnapshotTests: XCTestCase {
textFieldState:
TCATextFieldReducer.State(
validationType: nil,
text: "utest1zkkkjfxkamagznjr6ayemffj2d2gacdwpzcyw669pvg06xevzqslpmm27zjsctlkstl2vsw62xrjktmzqcu4yu9zdhdxqz3kafa4j2q85y6mv74rzjcgjg8c0ytrg7dwyzwtgnuc76h".redacted
text:
// swiftlint:disable line_length
"utest1zkkkjfxkamagznjr6ayemffj2d2gacdwpzcyw669pvg06xevzqslpmm27zjsctlkstl2vsw62xrjktmzqcu4yu9zdhdxqz3kafa4j2q85y6mv74rzjcgjg8c0ytrg7dwyzwtgnuc76h".redacted
)
),
transactionAmountInputState: TransactionAmountTextFieldReducer.State(
@ -109,7 +111,9 @@ class SendSnapshotTests: XCTestCase {
textFieldState:
TCATextFieldReducer.State(
validationType: nil,
text: "utest1zkkkjfxkamagznjr6ayemffj2d2gacdwpzcyw669pvg06xevzqslpmm27zjsctlkstl2vsw62xrjktmzqcu4yu9zdhdxqz3kafa4j2q85y6mv74rzjcgjg8c0ytrg7dwyzwtgnuc76h".redacted
text:
// swiftlint:disable line_length
"utest1zkkkjfxkamagznjr6ayemffj2d2gacdwpzcyw669pvg06xevzqslpmm27zjsctlkstl2vsw62xrjktmzqcu4yu9zdhdxqz3kafa4j2q85y6mv74rzjcgjg8c0ytrg7dwyzwtgnuc76h".redacted
)
),
transactionAmountInputState: TransactionAmountTextFieldReducer.State(

View File

@ -0,0 +1,100 @@
//
// SyncProgressTests.swift
// secantTests
//
// Created by Lukáš Korba on 21.12.2023.
//
import XCTest
import ComposableArchitecture
import ZcashLightClientKit
import SyncProgress
import Models
@testable import secant_testnet
@MainActor
final class SyncProgressTests: XCTestCase {
func testSyncingData() async throws {
let store = TestStore(
initialState: SyncProgressReducer.State(
lastKnownSyncPercentage: 0.0,
synchronizerStatusSnapshot: .snapshotFor(state: .syncing(0.513))
)
) {
SyncProgressReducer()
}
XCTAssertTrue(store.state.isSyncing)
XCTAssertEqual(store.state.syncingPercentage, 0.513 * 0.999)
}
func testlastKnownSyncingPercentage_Zero() async throws {
let store = TestStore(
initialState: SyncProgressReducer.State(
lastKnownSyncPercentage: 0.0,
synchronizerStatusSnapshot: .placeholder
)
) {
SyncProgressReducer()
}
XCTAssertEqual(store.state.lastKnownSyncPercentage, 0)
XCTAssertEqual(store.state.syncingPercentage, 0)
}
func testlastKnownSyncingPercentage_MoreThanZero() async throws {
let store = TestStore(
initialState: SyncProgressReducer.State(
lastKnownSyncPercentage: 0.15,
synchronizerStatusSnapshot: .placeholder
)
) {
SyncProgressReducer()
}
XCTAssertEqual(store.state.lastKnownSyncPercentage, 0.15)
XCTAssertEqual(store.state.syncingPercentage, 0.15)
}
func testlastKnownSyncingPercentage_FromSyncedState() async throws {
let store = TestStore(
initialState: SyncProgressReducer.State(
lastKnownSyncPercentage: 0.15,
synchronizerStatusSnapshot: .snapshotFor(state: .syncing(0.513))
)
) {
SyncProgressReducer()
}
var syncState: SynchronizerState = .zero
syncState.syncStatus = .upToDate
let snapshot = SyncStatusSnapshot.snapshotFor(state: syncState.syncStatus)
await store.send(.synchronizerStateChanged(syncState)) { state in
state.synchronizerStatusSnapshot = snapshot
state.syncStatusMessage = "Synced"
state.lastKnownSyncPercentage = 1.0
}
}
func testlastKnownSyncingPercentage_FromSyncingState() async throws {
let store = TestStore(
initialState: SyncProgressReducer.State(
lastKnownSyncPercentage: 0.15,
synchronizerStatusSnapshot: .snapshotFor(state: .syncing(0.513))
)
) {
SyncProgressReducer()
}
var syncState: SynchronizerState = .zero
syncState.syncStatus = .syncing(0.545)
let snapshot = SyncStatusSnapshot.snapshotFor(state: syncState.syncStatus)
await store.send(.synchronizerStateChanged(syncState)) { state in
state.synchronizerStatusSnapshot = snapshot
state.syncStatusMessage = "Syncing"
state.lastKnownSyncPercentage = 0.545
}
}
}

View File

@ -68,6 +68,33 @@ class TabsTests: XCTestCase {
}
}
func testRestoreWalletSubscription() async throws {
var initialState = TabsReducer.State.initial
initialState.isRestoringWallet = false
let store = TestStore(
initialState: initialState
) {
TabsReducer(tokenName: "TAZ", networkType: .testnet)
}
store.dependencies.restoreWalletStorage = .noOp
store.dependencies.restoreWalletStorage.value = {
AsyncStream { continuation in
continuation.yield(true)
continuation.finish()
}
}
await store.send(.restoreWalletTask)
await store.receive(.restoreWalletValue(true)) { state in
state.isRestoringWallet = true
}
await store.finish()
}
func testAccountTabTitle() {
var tabsState = TabsReducer.State.initial
tabsState.selectedTab = .account