404 lines
12 KiB
Swift
404 lines
12 KiB
Swift
import ComposableArchitecture
|
|
import SwiftUI
|
|
import ZcashLightClientKit
|
|
|
|
import UIKit
|
|
import AVFoundation
|
|
|
|
typealias HomeReducer = Reducer<HomeState, HomeAction, HomeEnvironment>
|
|
typealias HomeStore = Store<HomeState, HomeAction>
|
|
typealias HomeViewStore = ViewStore<HomeState, HomeAction>
|
|
|
|
// MARK: State
|
|
|
|
struct HomeState: Equatable {
|
|
enum Route: Equatable {
|
|
case notEnoughFreeDiskSpace
|
|
case profile
|
|
case request
|
|
case send
|
|
case scan
|
|
case balanceBreakdown
|
|
}
|
|
|
|
var route: Route?
|
|
|
|
var balanceBreakdown: BalanceBreakdownState
|
|
var drawerOverlay: DrawerOverlay
|
|
var profileState: ProfileState
|
|
var requestState: RequestState
|
|
var requiredTransactionConfirmations = 0
|
|
var scanState: ScanState
|
|
var sendState: SendFlowState
|
|
var shieldedBalance: WalletBalance
|
|
var synchronizerStatusSnapshot: SyncStatusSnapshot
|
|
var walletEventsState: WalletEventsFlowState
|
|
// TODO [#311]: - Get the ZEC price from the SDK, https://github.com/zcash/secant-ios-wallet/issues/311
|
|
var zecPrice = Decimal(140.0)
|
|
|
|
var totalCurrencyBalance: Zatoshi {
|
|
Zatoshi.from(decimal: shieldedBalance.total.decimalValue.decimalValue * zecPrice)
|
|
}
|
|
|
|
var isDownloading: Bool {
|
|
if case .downloading = synchronizerStatusSnapshot.syncStatus {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
var isUpToDate: Bool {
|
|
if case .synced = synchronizerStatusSnapshot.syncStatus {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
// MARK: Action
|
|
|
|
enum HomeAction: Equatable {
|
|
case balanceBreakdown(BalanceBreakdownAction)
|
|
case debugMenuStartup
|
|
case onAppear
|
|
case onDisappear
|
|
case profile(ProfileAction)
|
|
case request(RequestAction)
|
|
case send(SendFlowAction)
|
|
case scan(ScanAction)
|
|
case synchronizerStateChanged(WrappedSDKSynchronizerState)
|
|
case walletEvents(WalletEventsFlowAction)
|
|
case updateDrawer(DrawerOverlay)
|
|
case updateRoute(HomeState.Route?)
|
|
case updateSynchronizerStatus
|
|
case updateWalletEvents([WalletEvent])
|
|
}
|
|
|
|
// MARK: Environment
|
|
|
|
struct HomeEnvironment {
|
|
let audioServices: WrappedAudioServices
|
|
let derivationTool: WrappedDerivationTool
|
|
let diskSpaceChecker: WrappedDiskSpaceChecker
|
|
let feedbackGenerator: WrappedFeedbackGenerator
|
|
let mnemonic: WrappedMnemonic
|
|
let scheduler: AnySchedulerOf<DispatchQueue>
|
|
let SDKSynchronizer: WrappedSDKSynchronizer
|
|
let walletStorage: WrappedWalletStorage
|
|
let zcashSDKEnvironment: ZCashSDKEnvironment
|
|
}
|
|
|
|
extension HomeEnvironment {
|
|
static let demo = HomeEnvironment(
|
|
audioServices: .silent,
|
|
derivationTool: .live(),
|
|
diskSpaceChecker: .mockEmptyDisk,
|
|
feedbackGenerator: .silent,
|
|
mnemonic: .mock,
|
|
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
|
|
SDKSynchronizer: MockWrappedSDKSynchronizer(),
|
|
walletStorage: .throwing,
|
|
zcashSDKEnvironment: .testnet
|
|
)
|
|
}
|
|
|
|
// MARK: - Reducer
|
|
|
|
extension HomeReducer {
|
|
private struct CancelId: Hashable {}
|
|
|
|
static let `default` = HomeReducer.combine(
|
|
[
|
|
homeReducer,
|
|
historyReducer,
|
|
sendReducer,
|
|
scanReducer,
|
|
profileReducer,
|
|
balanceBreakdownReducer
|
|
]
|
|
)
|
|
|
|
private static let homeReducer = HomeReducer { state, action, environment in
|
|
switch action {
|
|
case .onAppear:
|
|
state.requiredTransactionConfirmations = environment.zcashSDKEnvironment.requiredTransactionConfirmations
|
|
|
|
if environment.diskSpaceChecker.hasEnoughFreeSpaceForSync() {
|
|
let syncEffect = environment.SDKSynchronizer.stateChanged
|
|
.map(HomeAction.synchronizerStateChanged)
|
|
.eraseToEffect()
|
|
.cancellable(id: CancelId(), cancelInFlight: true)
|
|
|
|
return .concatenate(Effect(value: .updateRoute(nil)), syncEffect)
|
|
} else {
|
|
return Effect(value: .updateRoute(.notEnoughFreeDiskSpace))
|
|
}
|
|
|
|
case .onDisappear:
|
|
return Effect.cancel(id: CancelId())
|
|
|
|
case .synchronizerStateChanged(.synced):
|
|
return .merge(
|
|
environment.SDKSynchronizer.getAllClearedTransactions()
|
|
.receive(on: environment.scheduler)
|
|
.map(HomeAction.updateWalletEvents)
|
|
.eraseToEffect(),
|
|
Effect(value: .updateSynchronizerStatus)
|
|
)
|
|
|
|
case .synchronizerStateChanged(let synchronizerState):
|
|
return Effect(value: .updateSynchronizerStatus)
|
|
|
|
case .updateDrawer(let drawerOverlay):
|
|
state.drawerOverlay = drawerOverlay
|
|
state.walletEventsState.isScrollable = drawerOverlay == .full ? true : false
|
|
return .none
|
|
|
|
case .updateWalletEvents(let walletEvents):
|
|
return .none
|
|
|
|
case .updateSynchronizerStatus:
|
|
state.synchronizerStatusSnapshot = environment.SDKSynchronizer.statusSnapshot()
|
|
if let shieldedBalance = environment.SDKSynchronizer.latestScannedSynchronizerState?.shieldedBalance {
|
|
state.shieldedBalance = shieldedBalance
|
|
}
|
|
return .none
|
|
|
|
case .updateRoute(let route):
|
|
state.route = route
|
|
return .none
|
|
|
|
case .profile(.back):
|
|
state.route = nil
|
|
return .none
|
|
|
|
case .profile(.settings(.quickRescan)):
|
|
do {
|
|
try environment.SDKSynchronizer.rewind(.quick)
|
|
} catch {
|
|
// TODO [#221]: error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
|
|
}
|
|
state.route = nil
|
|
return .none
|
|
|
|
case .profile(.settings(.fullRescan)):
|
|
do {
|
|
try environment.SDKSynchronizer.rewind(.birthday)
|
|
} catch {
|
|
// TODO [#221]: error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
|
|
}
|
|
state.route = nil
|
|
return .none
|
|
|
|
case .profile(let action):
|
|
return .none
|
|
|
|
case .request(let action):
|
|
return .none
|
|
|
|
case .walletEvents(.updateRoute(.all)):
|
|
return state.drawerOverlay != .full ? Effect(value: .updateDrawer(.full)) : .none
|
|
|
|
case .walletEvents(.updateRoute(.latest)):
|
|
return state.drawerOverlay != .partial ? Effect(value: .updateDrawer(.partial)) : .none
|
|
|
|
case .walletEvents(let historyAction):
|
|
return .none
|
|
|
|
case .send(.updateRoute(.done)):
|
|
return Effect(value: .updateRoute(nil))
|
|
|
|
case .send(let action):
|
|
return .none
|
|
|
|
case .scan(.found(let code)):
|
|
environment.audioServices.systemSoundVibrate()
|
|
return Effect(value: .updateRoute(nil))
|
|
|
|
case .scan(let action):
|
|
return .none
|
|
|
|
case .balanceBreakdown(.onDisappear):
|
|
state.route = nil
|
|
return .none
|
|
|
|
case .balanceBreakdown:
|
|
return .none
|
|
|
|
case .debugMenuStartup:
|
|
return .none
|
|
}
|
|
}
|
|
|
|
private static let historyReducer: HomeReducer = WalletEventsFlowReducer.default.pullback(
|
|
state: \HomeState.walletEventsState,
|
|
action: /HomeAction.walletEvents,
|
|
environment: { environment in
|
|
WalletEventsFlowEnvironment(
|
|
pasteboard: .live,
|
|
scheduler: environment.scheduler,
|
|
SDKSynchronizer: environment.SDKSynchronizer,
|
|
zcashSDKEnvironment: environment.zcashSDKEnvironment
|
|
)
|
|
}
|
|
)
|
|
|
|
private static let sendReducer: HomeReducer = SendFlowReducer.default.pullback(
|
|
state: \HomeState.sendState,
|
|
action: /HomeAction.send,
|
|
environment: { environment in
|
|
SendFlowEnvironment(
|
|
derivationTool: environment.derivationTool,
|
|
mnemonic: environment.mnemonic,
|
|
numberFormatter: .live(),
|
|
SDKSynchronizer: environment.SDKSynchronizer,
|
|
scheduler: environment.scheduler,
|
|
walletStorage: environment.walletStorage,
|
|
zcashSDKEnvironment: environment.zcashSDKEnvironment
|
|
)
|
|
}
|
|
)
|
|
|
|
private static let scanReducer: HomeReducer = ScanReducer.default.pullback(
|
|
state: \HomeState.scanState,
|
|
action: /HomeAction.scan,
|
|
environment: { environment in
|
|
ScanEnvironment(
|
|
captureDevice: .real,
|
|
scheduler: environment.scheduler,
|
|
uriParser: .live(uriParser: URIParser(derivationTool: environment.derivationTool))
|
|
)
|
|
}
|
|
)
|
|
|
|
private static let profileReducer: HomeReducer = ProfileReducer.default.pullback(
|
|
state: \HomeState.profileState,
|
|
action: /HomeAction.profile,
|
|
environment: { environment in
|
|
ProfileEnvironment(
|
|
appVersionHandler: .live,
|
|
mnemonic: environment.mnemonic,
|
|
SDKSynchronizer: environment.SDKSynchronizer,
|
|
walletStorage: environment.walletStorage,
|
|
zcashSDKEnvironment: environment.zcashSDKEnvironment
|
|
)
|
|
}
|
|
)
|
|
|
|
private static let balanceBreakdownReducer: HomeReducer = BalanceBreakdownReducer.default.pullback(
|
|
state: \HomeState.balanceBreakdown,
|
|
action: /HomeAction.balanceBreakdown,
|
|
environment: { environment in
|
|
BalanceBreakdownEnvironment(
|
|
numberFormatter: .live(),
|
|
SDKSynchronizer: environment.SDKSynchronizer,
|
|
scheduler: environment.scheduler
|
|
)
|
|
}
|
|
)
|
|
}
|
|
|
|
// MARK: - Store
|
|
|
|
extension HomeStore {
|
|
func historyStore() -> WalletEventsFlowStore {
|
|
self.scope(
|
|
state: \.walletEventsState,
|
|
action: HomeAction.walletEvents
|
|
)
|
|
}
|
|
|
|
func profileStore() -> ProfileStore {
|
|
self.scope(
|
|
state: \.profileState,
|
|
action: HomeAction.profile
|
|
)
|
|
}
|
|
|
|
func requestStore() -> RequestStore {
|
|
self.scope(
|
|
state: \.requestState,
|
|
action: HomeAction.request
|
|
)
|
|
}
|
|
|
|
func sendStore() -> SendFlowStore {
|
|
self.scope(
|
|
state: \.sendState,
|
|
action: HomeAction.send
|
|
)
|
|
}
|
|
|
|
func scanStore() -> ScanStore {
|
|
self.scope(
|
|
state: \.scanState,
|
|
action: HomeAction.scan
|
|
)
|
|
}
|
|
|
|
func balanceBreakdownStore() -> BalanceBreakdownStore {
|
|
self.scope(
|
|
state: \.balanceBreakdown,
|
|
action: HomeAction.balanceBreakdown
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - ViewStore
|
|
|
|
extension HomeViewStore {
|
|
func bindingForRoute(_ route: HomeState.Route) -> Binding<Bool> {
|
|
self.binding(
|
|
get: { $0.route == route },
|
|
send: { isActive in
|
|
return .updateRoute(isActive ? route : nil)
|
|
}
|
|
)
|
|
}
|
|
|
|
func bindingForDrawer() -> Binding<DrawerOverlay> {
|
|
self.binding(
|
|
get: { $0.drawerOverlay },
|
|
send: { .updateDrawer($0) }
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: Placeholders
|
|
|
|
extension HomeState {
|
|
static var placeholder: Self {
|
|
.init(
|
|
balanceBreakdown: .placeholder,
|
|
drawerOverlay: .partial,
|
|
profileState: .placeholder,
|
|
requestState: .placeholder,
|
|
scanState: .placeholder,
|
|
sendState: .placeholder,
|
|
shieldedBalance: WalletBalance.zero,
|
|
synchronizerStatusSnapshot: .default,
|
|
walletEventsState: .emptyPlaceHolder
|
|
)
|
|
}
|
|
}
|
|
|
|
extension HomeStore {
|
|
static var placeholder: HomeStore {
|
|
HomeStore(
|
|
initialState: .placeholder,
|
|
reducer: .default,
|
|
environment: HomeEnvironment(
|
|
audioServices: .silent,
|
|
derivationTool: .live(),
|
|
diskSpaceChecker: .mockEmptyDisk,
|
|
feedbackGenerator: .silent,
|
|
mnemonic: .live,
|
|
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
|
|
SDKSynchronizer: LiveWrappedSDKSynchronizer(),
|
|
walletStorage: .live(),
|
|
zcashSDKEnvironment: .testnet
|
|
)
|
|
)
|
|
}
|
|
}
|