* synchronizer status * amount input field enhancements Closes #302
This commit is contained in:
parent
f6e6f6991f
commit
6a12e09ee9
|
@ -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 */,
|
||||||
|
|
|
@ -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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,23 +2,37 @@ 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(
|
SingleLineTextField(
|
||||||
"ZEC Amount",
|
placeholderText: "0",
|
||||||
text: $transaction.amountString
|
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()
|
|
||||||
.background(Color.white)
|
|
||||||
.foregroundColor(Asset.Colors.Text.importSeedEditor.color)
|
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
|
|
||||||
|
@ -59,6 +73,7 @@ struct CreateTransaction: View {
|
||||||
.applyScreenBackground()
|
.applyScreenBackground()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Previews
|
// MARK: - Previews
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)")
|
||||||
|
|
|
@ -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: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
|
)
|
||||||
|
}
|
|
@ -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,11 +96,24 @@ 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)
|
||||||
})
|
|
||||||
|
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)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
try synchronizer?.prepare()
|
try synchronizer?.prepare()
|
||||||
|
@ -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,8 +251,10 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue