[#302] Synchronizer status on Home Screen (#304)

* synchronizer status

* amount input field enhancements

Closes #302
This commit is contained in:
Lukas Korba 2022-05-04 22:01:48 +02:00 committed by GitHub
parent f6e6f6991f
commit 6a12e09ee9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 365 additions and 94 deletions

View File

@ -106,6 +106,7 @@
9E5BF641281FD7B600BA3F17 /* TransactionFailedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF640281FD7B600BA3F17 /* TransactionFailedView.swift */; }; 9E5BF641281FD7B600BA3F17 /* TransactionFailedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF640281FD7B600BA3F17 /* TransactionFailedView.swift */; };
9E5BF644281FEC9900BA3F17 /* SendTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF643281FEC9900BA3F17 /* SendTests.swift */; }; 9E5BF644281FEC9900BA3F17 /* SendTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF643281FEC9900BA3F17 /* SendTests.swift */; };
9E5BF6462821028C00BA3F17 /* WrappedUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF6452821028C00BA3F17 /* WrappedUserDefaults.swift */; }; 9E5BF6462821028C00BA3F17 /* WrappedUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF6452821028C00BA3F17 /* WrappedUserDefaults.swift */; };
9E5BF648282277BE00BA3F17 /* WrappedNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF647282277BE00BA3F17 /* WrappedNotificationCenter.swift */; };
9E69A24D27FB002800A55317 /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E69A24C27FB002800A55317 /* Welcome.swift */; }; 9E69A24D27FB002800A55317 /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E69A24C27FB002800A55317 /* Welcome.swift */; };
9E80B47227E4B34B008FF493 /* UserPreferencesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */; }; 9E80B47227E4B34B008FF493 /* UserPreferencesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */; };
9EAFEB822805793200199FC9 /* AppReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB812805793200199FC9 /* AppReducerTests.swift */; }; 9EAFEB822805793200199FC9 /* AppReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB812805793200199FC9 /* AppReducerTests.swift */; };
@ -274,6 +275,7 @@
9E5BF640281FD7B600BA3F17 /* TransactionFailedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionFailedView.swift; sourceTree = "<group>"; }; 9E5BF640281FD7B600BA3F17 /* TransactionFailedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionFailedView.swift; sourceTree = "<group>"; };
9E5BF643281FEC9900BA3F17 /* SendTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTests.swift; sourceTree = "<group>"; }; 9E5BF643281FEC9900BA3F17 /* SendTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTests.swift; sourceTree = "<group>"; };
9E5BF6452821028C00BA3F17 /* WrappedUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedUserDefaults.swift; sourceTree = "<group>"; }; 9E5BF6452821028C00BA3F17 /* WrappedUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedUserDefaults.swift; sourceTree = "<group>"; };
9E5BF647282277BE00BA3F17 /* WrappedNotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedNotificationCenter.swift; sourceTree = "<group>"; };
9E69A24C27FB002800A55317 /* Welcome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = "<group>"; }; 9E69A24C27FB002800A55317 /* Welcome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = "<group>"; };
9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorage.swift; sourceTree = "<group>"; }; 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorage.swift; sourceTree = "<group>"; };
9EAFEB812805793200199FC9 /* AppReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducerTests.swift; sourceTree = "<group>"; }; 9EAFEB812805793200199FC9 /* AppReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducerTests.swift; sourceTree = "<group>"; };
@ -762,6 +764,7 @@
9E02B5C2280458D2005B809B /* WrappedDerivationTool.swift */, 9E02B5C2280458D2005B809B /* WrappedDerivationTool.swift */,
9EAFEB872806E5AE00199FC9 /* WrappedSDKSynchronizer.swift */, 9EAFEB872806E5AE00199FC9 /* WrappedSDKSynchronizer.swift */,
9E5BF6452821028C00BA3F17 /* WrappedUserDefaults.swift */, 9E5BF6452821028C00BA3F17 /* WrappedUserDefaults.swift */,
9E5BF647282277BE00BA3F17 /* WrappedNotificationCenter.swift */,
); );
path = Wrappers; path = Wrappers;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1232,6 +1235,7 @@
0DC487C32772574C00BE6A63 /* ValidationSucceededView.swift in Sources */, 0DC487C32772574C00BE6A63 /* ValidationSucceededView.swift in Sources */,
2EB1C5E827D77F6100BC43D7 /* TextFieldStore.swift in Sources */, 2EB1C5E827D77F6100BC43D7 /* TextFieldStore.swift in Sources */,
9EAFEB8A2806F48100199FC9 /* ZCashSDKEnvironment.swift in Sources */, 9EAFEB8A2806F48100199FC9 /* ZCashSDKEnvironment.swift in Sources */,
9E5BF648282277BE00BA3F17 /* WrappedNotificationCenter.swift in Sources */,
0D8A43C4272AEEDE005A6414 /* SecantTextStyles.swift in Sources */, 0D8A43C4272AEEDE005A6414 /* SecantTextStyles.swift in Sources */,
9E5BF641281FD7B600BA3F17 /* TransactionFailedView.swift in Sources */, 9E5BF641281FD7B600BA3F17 /* TransactionFailedView.swift in Sources */,
9E4DC6E027C409A100E657F4 /* NeumorphicDesignModifier.swift in Sources */, 9E4DC6E027C409A100E657F4 /* NeumorphicDesignModifier.swift in Sources */,

View File

@ -1,5 +1,6 @@
import ComposableArchitecture import ComposableArchitecture
import SwiftUI import SwiftUI
import ZcashLightClientKit
struct HomeState: Equatable { struct HomeState: Equatable {
enum Route: Equatable { enum Route: Equatable {
@ -16,6 +17,7 @@ struct HomeState: Equatable {
var requestState: RequestState var requestState: RequestState
var sendState: SendState var sendState: SendState
var scanState: ScanState var scanState: ScanState
var synchronizerStatus: String
var totalBalance: Double var totalBalance: Double
var transactionHistoryState: TransactionHistoryState var transactionHistoryState: TransactionHistoryState
var verifiedBalance: Double var verifiedBalance: Double
@ -34,6 +36,7 @@ enum HomeAction: Equatable {
case updateBalance(Balance) case updateBalance(Balance)
case updateDrawer(DrawerOverlay) case updateDrawer(DrawerOverlay)
case updateRoute(HomeState.Route?) case updateRoute(HomeState.Route?)
case updateSynchronizerStatus
case updateTransactions([TransactionState]) case updateTransactions([TransactionState])
} }
@ -83,20 +86,19 @@ extension HomeReducer {
.receive(on: environment.scheduler) .receive(on: environment.scheduler)
.map({ Balance(verified: $0.verified, total: $0.total) }) .map({ Balance(verified: $0.verified, total: $0.total) })
.map(HomeAction.updateBalance) .map(HomeAction.updateBalance)
.eraseToEffect() .eraseToEffect(),
Effect(value: .updateSynchronizerStatus)
) )
case .synchronizerStateChanged(let synchronizerState): case .synchronizerStateChanged(let synchronizerState):
return .none return Effect(value: .updateSynchronizerStatus)
case .updateBalance(let balance): case .updateBalance(let balance):
state.totalBalance = balance.total.asHumanReadableZecBalance() state.totalBalance = balance.total.asHumanReadableZecBalance()
state.verifiedBalance = balance.verified.asHumanReadableZecBalance() state.verifiedBalance = balance.verified.asHumanReadableZecBalance()
return .none return .none
case .debugMenuStartup:
return .none
case .updateDrawer(let drawerOverlay): case .updateDrawer(let drawerOverlay):
state.drawerOverlay = drawerOverlay state.drawerOverlay = drawerOverlay
state.transactionHistoryState.isScrollable = drawerOverlay == .full ? true : false state.transactionHistoryState.isScrollable = drawerOverlay == .full ? true : false
@ -105,6 +107,10 @@ extension HomeReducer {
case .updateTransactions(let transactions): case .updateTransactions(let transactions):
return .none return .none
case .updateSynchronizerStatus:
state.synchronizerStatus = environment.wrappedSDKSynchronizer.status()
return .none
case .updateRoute(let route): case .updateRoute(let route):
state.route = route state.route = route
return .none return .none
@ -132,6 +138,9 @@ extension HomeReducer {
case .send(let action): case .send(let action):
return .none return .none
case .debugMenuStartup:
return .none
} }
} }
@ -234,9 +243,46 @@ extension HomeState {
requestState: .placeholder, requestState: .placeholder,
sendState: .placeholder, sendState: .placeholder,
scanState: .placeholder, scanState: .placeholder,
synchronizerStatus: "",
totalBalance: 0.0, totalBalance: 0.0,
transactionHistoryState: .emptyPlaceHolder, transactionHistoryState: .emptyPlaceHolder,
verifiedBalance: 0.0 verifiedBalance: 0.0
) )
} }
} }
extension SDKSynchronizer {
static func textFor(state: SyncStatus) -> String {
switch state {
case .downloading(let progress):
return "Downloading \(progress.progressHeight)/\(progress.targetHeight)"
case .enhancing(let enhanceProgress):
return "Enhancing tx \(enhanceProgress.enhancedTransactions) of \(enhanceProgress.totalTransactions)"
case .fetching:
return "fetching UTXOs"
case .scanning(let scanProgress):
return "Scanning: \(scanProgress.progressHeight)/\(scanProgress.targetHeight)"
case .disconnected:
return "disconnected 💔"
case .stopped:
return "Stopped 🚫"
case .synced:
return "Synced 😎"
case .unprepared:
return "Unprepared 😅"
case .validating:
return "Validating"
case .error(let err):
return "Error: \(err.localizedDescription)"
}
}
}

View File

@ -15,11 +15,14 @@ struct HomeView: View {
sendButton(viewStore) sendButton(viewStore)
VStack { VStack {
Text("\(viewStore.synchronizerStatus)")
.padding(.top, 60)
Text("balance: \(viewStore.totalBalance)") Text("balance: \(viewStore.totalBalance)")
.accessDebugMenuWithHiddenGesture { .accessDebugMenuWithHiddenGesture {
viewStore.send(.debugMenuStartup) viewStore.send(.debugMenuStartup)
} }
.padding(.top, 180) .padding(.top, 120)
Spacer() Spacer()
} }

View File

@ -19,7 +19,6 @@ struct SandboxView: View {
case .history: case .history:
TransactionHistoryView(store: store.historyStore()) TransactionHistoryView(store: store.historyStore())
case .send: case .send:
EmptyView()
SendView( SendView(
store: .init( store: .init(
initialState: .placeholder, initialState: .placeholder,

View File

@ -35,12 +35,19 @@ struct SendState: Equatable {
var route: Route? var route: Route?
var isSendingTransaction = false var isSendingTransaction = false
var totalBalance = 0.0
var transaction: Transaction var transaction: Transaction
var transactionInputState: TransactionInputState
} }
enum SendAction: Equatable { enum SendAction: Equatable {
case onAppear
case onDisappear
case sendConfirmationPressed case sendConfirmationPressed
case sendTransactionResult(Result<TransactionState, NSError>) case sendTransactionResult(Result<TransactionState, NSError>)
case synchronizerStateChanged(WrappedSDKSynchronizerState)
case transactionInput(TransactionInputAction)
case updateBalance(Double)
case updateTransaction(Transaction) case updateTransaction(Transaction)
case updateRoute(SendState.Route?) case updateRoute(SendState.Route?)
} }
@ -55,12 +62,23 @@ struct SendEnvironment {
// MARK: - SendReducer // MARK: - SendReducer
private struct ListenerId: Hashable {}
typealias SendReducer = Reducer<SendState, SendAction, SendEnvironment> typealias SendReducer = Reducer<SendState, SendAction, SendEnvironment>
extension SendReducer { extension SendReducer {
private struct SyncStatusUpdatesID: Hashable {} private struct SyncStatusUpdatesID: Hashable {}
static let `default` = Reducer<SendState, SendAction, SendEnvironment> { state, action, environment in static let `default` = SendReducer.combine(
[
balanceReducer,
sendReducer,
transactionInputReducer
]
)
.debug()
private static let sendReducer = SendReducer { state, action, environment in
switch action { switch action {
case let .updateTransaction(transaction): case let .updateTransaction(transaction):
state.transaction = transaction state.transaction = transaction
@ -111,9 +129,51 @@ extension SendReducer {
} catch { } catch {
return Effect(value: .updateRoute(.failure)) return Effect(value: .updateRoute(.failure))
} }
case .transactionInput(let transactionInput):
return .none
default:
return .none
} }
} }
private static let balanceReducer = SendReducer { state, action, environment in
switch action {
case .onAppear:
return environment.wrappedSDKSynchronizer.stateChanged
.map(SendAction.synchronizerStateChanged)
.eraseToEffect()
.cancellable(id: ListenerId(), cancelInFlight: true)
case .onDisappear:
return Effect.cancel(id: ListenerId())
case .synchronizerStateChanged(.synced):
return environment.wrappedSDKSynchronizer.getShieldedBalance()
.receive(on: environment.scheduler)
.map({ Double($0.total) / Double(100_000_000) })
.map(SendAction.updateBalance)
.eraseToEffect()
case .synchronizerStateChanged(let synchronizerState):
return .none
case .updateBalance(let balance):
state.totalBalance = balance
state.transactionInputState.maxValue = Int64(balance * 100_000_000)
return .none
default:
return .none
}
}
private static let transactionInputReducer: SendReducer = TransactionInputReducer.default.pullback(
state: \SendState.transactionInputState,
action: /SendAction.transactionInput,
environment: { _ in TransactionInputEnvironment() }
)
static func `default`(whenDone: @escaping () -> Void) -> SendReducer { static func `default`(whenDone: @escaping () -> Void) -> SendReducer {
SendReducer { state, action, environment in SendReducer { state, action, environment in
switch action { switch action {
@ -176,13 +236,24 @@ extension SendViewStore {
embed: { $0 ? SendState.Route.done : SendState.Route.showConfirmation } embed: { $0 ? SendState.Route.done : SendState.Route.showConfirmation }
) )
} }
var bindingForBalance: Binding<Double> {
self.binding(
get: \.totalBalance,
send: SendAction.updateBalance
)
}
} }
// MARK: PlaceHolders // MARK: PlaceHolders
extension SendState { extension SendState {
static var placeholder: Self { static var placeholder: Self {
.init(route: nil, transaction: .placeholder) .init(
route: nil,
transaction: .placeholder,
transactionInputState: .placeholer
)
} }
static var emptyPlaceholder: Self { static var emptyPlaceholder: Self {
@ -192,7 +263,8 @@ extension SendState {
amount: 0, amount: 0,
memo: "", memo: "",
toAddress: "" toAddress: ""
) ),
transactionInputState: .placeholer
) )
} }
} }

View File

@ -2,61 +2,76 @@ import SwiftUI
import ComposableArchitecture import ComposableArchitecture
struct CreateTransaction: View { struct CreateTransaction: View {
let store: TransactionInputStore
@Binding var transaction: Transaction @Binding var transaction: Transaction
@Binding var isComplete: Bool @Binding var isComplete: Bool
@Binding var totalBalance: Double
var body: some View { var body: some View {
UITextView.appearance().backgroundColor = .clear UITextView.appearance().backgroundColor = .clear
return VStack { return WithViewStore(store) { viewStore in
VStack { VStack {
Text("ZEC Amount") VStack {
Text("Balance \(totalBalance)")
TextField(
"ZEC Amount", SingleLineTextField(
text: $transaction.amountString placeholderText: "0",
) title: "How much ZEC would you like to send?",
store: store.scope(
state: \.textFieldState,
action: TransactionInputAction.textField
),
titleAccessoryView: {
Button(
action: { viewStore.send(.setMax(viewStore.maxValue)) },
label: { Text("Max") }
)
.textFieldTitleAccessoryButtonStyle
},
inputAccessoryView: {
}
)
}
.padding() .padding()
.background(Color.white)
.foregroundColor(Asset.Colors.Text.importSeedEditor.color)
}
.padding()
VStack {
Text("To Address")
TextField( VStack {
"Address", Text("To Address")
text: $transaction.toAddress
) TextField(
.font(.system(size: 14)) "Address",
text: $transaction.toAddress
)
.font(.system(size: 14))
.padding()
.background(Color.white)
.foregroundColor(Asset.Colors.Text.importSeedEditor.color)
}
.padding() .padding()
.background(Color.white)
.foregroundColor(Asset.Colors.Text.importSeedEditor.color)
}
.padding()
VStack {
Text("Memo")
TextEditor(text: $transaction.memo) VStack {
.frame(maxWidth: .infinity, maxHeight: 150, alignment: .center) Text("Memo")
.importSeedEditorModifier()
TextEditor(text: $transaction.memo)
.frame(maxWidth: .infinity, maxHeight: 150, alignment: .center)
.importSeedEditorModifier()
}
.padding()
Button(
action: { isComplete = true },
label: { Text("Send") }
)
.activeButtonStyle
.frame(height: 50)
.padding()
Spacer()
} }
.padding() .padding()
.applyScreenBackground()
Button(
action: { isComplete = true },
label: { Text("Send") }
)
.activeButtonStyle
.frame(height: 50)
.padding()
Spacer()
} }
.padding()
.applyScreenBackground()
} }
} }
@ -68,12 +83,15 @@ struct Create_Previews: PreviewProvider {
StateContainer( StateContainer(
initialState: ( initialState: (
Transaction.placeholder, Transaction.placeholder,
false false,
0.0
) )
) { ) {
CreateTransaction( CreateTransaction(
store: .placeholder,
transaction: $0.0, transaction: $0.0,
isComplete: $0.1 isComplete: $0.1,
totalBalance: $0.2
) )
} }
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
@ -88,7 +106,8 @@ extension SendStore {
return SendStore( return SendStore(
initialState: .init( initialState: .init(
route: nil, route: nil,
transaction: .placeholder transaction: .placeholder,
transactionInputState: .placeholer
), ),
reducer: .default, reducer: .default,
environment: SendEnvironment( environment: SendEnvironment(

View File

@ -7,9 +7,16 @@ struct SendView: View {
var body: some View { var body: some View {
WithViewStore(store) { viewStore in WithViewStore(store) { viewStore in
CreateTransaction( CreateTransaction(
store: store.scope(
state: \.transactionInputState,
action: SendAction.transactionInput
),
transaction: viewStore.bindingForTransaction, transaction: viewStore.bindingForTransaction,
isComplete: viewStore.bindingForConfirmation isComplete: viewStore.bindingForConfirmation,
totalBalance: viewStore.bindingForBalance
) )
.onAppear { viewStore.send(.onAppear) }
.onDisappear { viewStore.send(.onDisappear) }
.navigationLinkEmpty( .navigationLinkEmpty(
isActive: viewStore.bindingForConfirmation, isActive: viewStore.bindingForConfirmation,
destination: { destination: {
@ -35,7 +42,8 @@ struct SendView_Previews: PreviewProvider {
store: .init( store: .init(
initialState: .init( initialState: .init(
route: nil, route: nil,
transaction: .placeholder transaction: .placeholder,
transactionInputState: .placeholer
), ),
reducer: .default, reducer: .default,
environment: SendEnvironment( environment: SendEnvironment(

View File

@ -6,7 +6,7 @@ struct TransactionConfirmation: View {
var body: some View { var body: some View {
VStack { VStack {
Text("Send \(String(format: "%.7f", Int64(viewStore.transaction.amount).asHumanReadableZecBalance())) ZEC") Text("Send \(String(format: "%.7f", Int64(viewStore.transactionInputState.amount).asHumanReadableZecBalance())) ZEC")
.padding() .padding()
Text("To \(viewStore.transaction.toAddress)") Text("To \(viewStore.transaction.toAddress)")

View File

@ -21,8 +21,8 @@ struct TextFieldState: Equatable {
} }
} }
enum TextFieldAction { enum TextFieldAction: Equatable {
case apply((String) -> String) // case apply((String) -> String)
case set(String) case set(String)
} }
@ -31,10 +31,9 @@ struct TextFieldEnvironment: Equatable { }
extension TextFieldReducer { extension TextFieldReducer {
static let `default` = TextFieldReducer { state, action, _ in static let `default` = TextFieldReducer { state, action, _ in
switch action { switch action {
case .apply(let action): // case .apply(let action):
state.text = action(state.text) // state.text = action(state.text)
state.valid = state.text.isValid(for: state.validationType) // state.valid = state.text.isValid(for: state.validationType)
case .set(let text): case .set(let text):
state.text = text state.text = text
state.valid = state.text.isValid(for: state.validationType) state.valid = state.text.isValid(for: state.validationType)
@ -60,3 +59,10 @@ extension TextFieldStore {
) )
} }
} }
extension TextFieldState {
static let placeholder = TextFieldState(
validationType: nil,
text: ""
)
}

View File

@ -18,30 +18,21 @@ typealias TransactionInputStore = Store<TransactionInputState, TransactionInputA
struct TransactionInputState: Equatable { struct TransactionInputState: Equatable {
var textFieldState: TextFieldState var textFieldState: TextFieldState
var currencySelectionState: CurrencySelectionState var currencySelectionState: CurrencySelectionState
var maxValue: Int64 = 0
var amount: Int64 {
Int64((Double(textFieldState.text) ?? 0.0) * 100_000_000)
}
} }
enum TransactionInputAction { enum TransactionInputAction: Equatable {
case setMax(Double) case setMax(Int64)
case textField(TextFieldAction) case textField(TextFieldAction)
case currencySelection(CurrencySelectionAction) case currencySelection(CurrencySelectionAction)
} }
struct TransactionInputEnvironment: Equatable {} struct TransactionInputEnvironment: Equatable {}
func maxOverride(_ reducer: @escaping (TransactionReducerData)) -> TransactionReducerData {
return { state, action in
switch action {
case .setMax(let value):
state.textFieldState.text = "\(value)"
state.currencySelectionState.currencyType = .usd
default: break
}
reducer(&state, action)
}
}
extension TransactionInputReducer { extension TransactionInputReducer {
static let `default` = TransactionInputReducer.combine( static let `default` = TransactionInputReducer.combine(
[ [
@ -56,7 +47,7 @@ extension TransactionInputReducer {
switch action { switch action {
case .setMax(let value): case .setMax(let value):
state.currencySelectionState.currencyType = .zec state.currencySelectionState.currencyType = .zec
state.textFieldState.text = "\(value)" state.textFieldState.text = "\(value.asHumanReadableZecBalance())"
default: break default: break
} }
@ -99,3 +90,18 @@ extension TransactionInputReducer {
environment: { _ in return .init() } environment: { _ in return .init() }
) )
} }
extension TransactionInputState {
static let placeholer = TransactionInputState(
textFieldState: .placeholder,
currencySelectionState: CurrencySelectionState()
)
}
extension TransactionInputStore {
static let placeholder = TransactionInputStore(
initialState: .placeholer,
reducer: .default,
environment: TransactionInputEnvironment()
)
}

View File

@ -14,7 +14,7 @@ struct TransactionTextField: View {
// Constant example used here, this could be injected by a dependency // Constant example used here, this could be injected by a dependency
// Access to this value could also be injected into the store as a dependency // Access to this value could also be injected into the store as a dependency
// with a function to prouce this value. // with a function to prouce this value.
let maxTransactionValue = 500.0 let maxTransactionValue: Int64 = 500
var body: some View { var body: some View {
WithViewStore(store) { viewStore in WithViewStore(store) { viewStore in
@ -68,5 +68,41 @@ struct TransactionTextField_Previews: PreviewProvider {
.padding(.horizontal, 50) .padding(.horizontal, 50)
.applyScreenBackground() .applyScreenBackground()
.previewLayout(.fixed(width: 500, height: 200)) .previewLayout(.fixed(width: 500, height: 200))
SingleLineTextField(
placeholderText: "$0",
title: "How much?",
store: .transaction,
titleAccessoryView: {
Button(
action: { },
label: { Text("Max") }
)
.textFieldTitleAccessoryButtonStyle
},
inputAccessoryView: {
}
)
.preferredColorScheme(.dark)
.padding(.horizontal, 50)
.applyScreenBackground()
.previewLayout(.fixed(width: 500, height: 200))
SingleLineTextField(
placeholderText: "",
title: "Address",
store: .address,
titleAccessoryView: {
},
inputAccessoryView: {
Image(Asset.Assets.Icons.qrCode.name)
.resizable()
.frame(width: 30, height: 30)
}
)
.preferredColorScheme(.dark)
.padding(.horizontal, 50)
.applyScreenBackground()
.previewLayout(.fixed(width: 500, height: 200))
} }
} }

View File

@ -0,0 +1,22 @@
//
// WrappedNotificationCenter.swift
// secant-testnet
//
// Created by Lukáš Korba on 04.05.2022.
//
import Foundation
struct WrappedNotificationCenter {
let publisherFor: (Notification.Name) -> NotificationCenter.Publisher?
}
extension WrappedNotificationCenter {
static let live = WrappedNotificationCenter(
publisherFor: { NotificationCenter.default.publisher(for: $0) }
)
static let mock = WrappedNotificationCenter(
publisherFor: { _ in nil }
)
}

View File

@ -40,11 +40,12 @@ struct Balance: WalletBalance, Equatable {
protocol WrappedSDKSynchronizer { protocol WrappedSDKSynchronizer {
var synchronizer: SDKSynchronizer? { get } var synchronizer: SDKSynchronizer? { get }
var stateChanged: CurrentValueSubject<WrappedSDKSynchronizerState, Never> { get } var stateChanged: CurrentValueSubject<WrappedSDKSynchronizerState, Never> { get }
var notificationCenter: WrappedNotificationCenter { get }
func prepareWith(initializer: Initializer) throws func prepareWith(initializer: Initializer) throws
func start(retry: Bool) throws func start(retry: Bool) throws
func stop() func stop()
func synchronizerSynced() func status() -> String
func getShieldedBalance() -> Effect<Balance, Never> func getShieldedBalance() -> Effect<Balance, Never>
func getAllClearedTransactions() -> Effect<[TransactionState], Never> func getAllClearedTransactions() -> Effect<[TransactionState], Never>
@ -81,8 +82,10 @@ class LiveWrappedSDKSynchronizer: WrappedSDKSynchronizer {
private var cancellables: [AnyCancellable] = [] private var cancellables: [AnyCancellable] = []
private(set) var synchronizer: SDKSynchronizer? private(set) var synchronizer: SDKSynchronizer?
private(set) var stateChanged: CurrentValueSubject<WrappedSDKSynchronizerState, Never> private(set) var stateChanged: CurrentValueSubject<WrappedSDKSynchronizerState, Never>
private(set) var notificationCenter: WrappedNotificationCenter
init() { init(notificationCenter: WrappedNotificationCenter = .live) {
self.notificationCenter = notificationCenter
self.stateChanged = CurrentValueSubject<WrappedSDKSynchronizerState, Never>(.unknown) self.stateChanged = CurrentValueSubject<WrappedSDKSynchronizerState, Never>(.unknown)
} }
@ -93,16 +96,29 @@ class LiveWrappedSDKSynchronizer: WrappedSDKSynchronizer {
func prepareWith(initializer: Initializer) throws { func prepareWith(initializer: Initializer) throws {
synchronizer = try SDKSynchronizer(initializer: initializer) synchronizer = try SDKSynchronizer(initializer: initializer)
NotificationCenter.default.publisher(for: .synchronizerSynced) notificationCenter.publisherFor(.synchronizerStarted)?
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] _ in .sink { [weak self] _ in self?.synchronizerStarted() }
self?.synchronizerSynced()
})
.store(in: &cancellables) .store(in: &cancellables)
notificationCenter.publisherFor(.synchronizerSynced)?
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in self?.synchronizerSynced() }
.store(in: &cancellables)
notificationCenter.publisherFor(.synchronizerProgressUpdated)?
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in self?.synchronizerProgressUpdated() }
.store(in: &cancellables)
notificationCenter.publisherFor(.synchronizerStopped)?
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in self?.synchronizerStopped() }
.store(in: &cancellables)
try synchronizer?.prepare() try synchronizer?.prepare()
} }
func start(retry: Bool) throws { func start(retry: Bool) throws {
try synchronizer?.start(retry: retry) try synchronizer?.start(retry: retry)
} }
@ -111,10 +127,30 @@ class LiveWrappedSDKSynchronizer: WrappedSDKSynchronizer {
synchronizer?.stop() synchronizer?.stop()
} }
func synchronizerStarted() {
stateChanged.send(.started)
}
func synchronizerSynced() { func synchronizerSynced() {
stateChanged.send(.synced) stateChanged.send(.synced)
} }
func synchronizerProgressUpdated() {
stateChanged.send(.progressUpdated)
}
func synchronizerStopped() {
stateChanged.send(.stopped)
}
func status() -> String {
guard let synchronizer = synchronizer else {
return ""
}
return SDKSynchronizer.textFor(state: synchronizer.status)
}
func getShieldedBalance() -> Effect<Balance, Never> { func getShieldedBalance() -> Effect<Balance, Never> {
if let shieldedVerifiedBalance = synchronizer?.getShieldedVerifiedBalance(), if let shieldedVerifiedBalance = synchronizer?.getShieldedVerifiedBalance(),
let shieldedTotalBalance = synchronizer?.getShieldedBalance(accountIndex: 0) { let shieldedTotalBalance = synchronizer?.getShieldedBalance(accountIndex: 0) {
@ -215,11 +251,13 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer {
private var cancellables: [AnyCancellable] = [] private var cancellables: [AnyCancellable] = []
private(set) var synchronizer: SDKSynchronizer? private(set) var synchronizer: SDKSynchronizer?
private(set) var stateChanged: CurrentValueSubject<WrappedSDKSynchronizerState, Never> private(set) var stateChanged: CurrentValueSubject<WrappedSDKSynchronizerState, Never>
private(set) var notificationCenter: WrappedNotificationCenter
init() { init(notificationCenter: WrappedNotificationCenter = .mock) {
self.notificationCenter = notificationCenter
self.stateChanged = CurrentValueSubject<WrappedSDKSynchronizerState, Never>(.unknown) self.stateChanged = CurrentValueSubject<WrappedSDKSynchronizerState, Never>(.unknown)
} }
deinit { deinit {
synchronizer?.stop() synchronizer?.stop()
} }
@ -249,6 +287,14 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer {
stateChanged.send(.synced) stateChanged.send(.synced)
} }
func status() -> String {
guard let synchronizer = synchronizer else {
return ""
}
return SDKSynchronizer.textFor(state: synchronizer.status)
}
func getShieldedBalance() -> Effect<Balance, Never> { func getShieldedBalance() -> Effect<Balance, Never> {
return Effect(value: Balance(verified: 12345000, total: 12345000)) return Effect(value: Balance(verified: 12345000, total: 12345000))
} }
@ -339,8 +385,10 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer {
class TestWrappedSDKSynchronizer: WrappedSDKSynchronizer { class TestWrappedSDKSynchronizer: WrappedSDKSynchronizer {
private(set) var synchronizer: SDKSynchronizer? private(set) var synchronizer: SDKSynchronizer?
private(set) var stateChanged: CurrentValueSubject<WrappedSDKSynchronizerState, Never> private(set) var stateChanged: CurrentValueSubject<WrappedSDKSynchronizerState, Never>
private(set) var notificationCenter: WrappedNotificationCenter
init() { init(notificationCenter: WrappedNotificationCenter = .mock) {
self.notificationCenter = notificationCenter
self.stateChanged = CurrentValueSubject<WrappedSDKSynchronizerState, Never>(.unknown) self.stateChanged = CurrentValueSubject<WrappedSDKSynchronizerState, Never>(.unknown)
} }
@ -352,6 +400,8 @@ class TestWrappedSDKSynchronizer: WrappedSDKSynchronizer {
func synchronizerSynced() { } func synchronizerSynced() { }
func status() -> String { "" }
func getShieldedBalance() -> Effect<Balance, Never> { func getShieldedBalance() -> Effect<Balance, Never> {
return .none return .none
} }

View File

@ -19,7 +19,7 @@ class UserPreferencesStorageTests: XCTestCase {
super.setUp() super.setUp()
guard let userDefaults = UserDefaults.init(suiteName: "test") else { guard let userDefaults = UserDefaults.init(suiteName: "test") else {
XCTFail("UserPreferencesStorageTests: UserDefaults.init(suiteName: "test") failed to initialize") XCTFail("UserPreferencesStorageTests: UserDefaults.init(suiteName: \"test\") failed to initialize")
return return
} }
@ -214,7 +214,7 @@ class UserPreferencesStorageTests: XCTestCase {
func testRemoveAll() throws { func testRemoveAll() throws {
guard let userDefaults = UserDefaults.init(suiteName: "test") else { guard let userDefaults = UserDefaults.init(suiteName: "test") else {
XCTFail("User Preferences: UserDefaults.init(suiteName: "test") failed to initialize") XCTFail("User Preferences: UserDefaults.init(suiteName: \"test\") failed to initialize")
return return
} }