308 lines
11 KiB
Swift
308 lines
11 KiB
Swift
//
|
|
// AddressBookStore.swift
|
|
// Zashi
|
|
//
|
|
// Created by Lukáš Korba on 05-28-2024.
|
|
//
|
|
|
|
import ComposableArchitecture
|
|
import SwiftUI
|
|
|
|
import AddressBookClient
|
|
import DerivationTool
|
|
import Generated
|
|
import Models
|
|
import ZcashSDKEnvironment
|
|
import AudioServices
|
|
import ZcashLightClientKit
|
|
|
|
@Reducer
|
|
public struct AddressBook {
|
|
enum Constants {
|
|
static let maxNameLength = 32
|
|
}
|
|
|
|
@ObservableState
|
|
public struct State: Equatable {
|
|
public var address = ""
|
|
public var addressAlreadyExists = false
|
|
@Shared(.inMemory(.addressBookContacts)) public var addressBookContacts: AddressBookContacts = .empty
|
|
@Presents public var alert: AlertState<Action>?
|
|
public var deleteIdToConfirm: String?
|
|
public var isAddressFocused = false
|
|
public var editId: String?
|
|
public var isInSelectMode = false
|
|
public var isNameFocused = false
|
|
public var isValidForm = false
|
|
public var isValidZcashAddress = false
|
|
public var name = ""
|
|
public var nameAlreadyExists = false
|
|
public var originalAddress = ""
|
|
public var originalName = ""
|
|
@Shared(.inMemory(.selectedWalletAccount)) public var selectedWalletAccount: WalletAccount? = nil
|
|
@Shared(.inMemory(.walletAccounts)) public var walletAccounts: [WalletAccount] = []
|
|
@Shared(.inMemory(.zashiWalletAccount)) public var zashiWalletAccount: WalletAccount? = nil
|
|
|
|
public var isEditChange: Bool {
|
|
(address != originalAddress || name != originalName) && isValidForm
|
|
}
|
|
|
|
public var isNameOverMaxLength: Bool {
|
|
name.count > Constants.maxNameLength
|
|
}
|
|
|
|
public var isSaveButtonDisabled: Bool {
|
|
!isEditChange
|
|
|| isNameOverMaxLength
|
|
|| nameAlreadyExists
|
|
|| addressAlreadyExists
|
|
}
|
|
|
|
public var invalidAddressErrorText: String? {
|
|
addressAlreadyExists
|
|
? L10n.AddressBook.Error.addressExists
|
|
: (isValidZcashAddress || address.isEmpty)
|
|
? nil
|
|
: L10n.AddressBook.Error.invalidAddress
|
|
}
|
|
|
|
public var invalidNameErrorText: String? {
|
|
isNameOverMaxLength
|
|
? L10n.AddressBook.Error.nameLength
|
|
: nameAlreadyExists
|
|
? L10n.AddressBook.Error.nameExists
|
|
: nil
|
|
}
|
|
|
|
public init() { }
|
|
}
|
|
|
|
public enum Action: BindableAction, Equatable {
|
|
case addManualButtonTapped
|
|
case alert(PresentationAction<Action>)
|
|
case binding(BindingAction<AddressBook.State>)
|
|
case contactStoreSuccess
|
|
case deleteId(String)
|
|
case deleteIdConfirmed
|
|
case dismissAddContactRequired
|
|
case dismissDeleteContactRequired
|
|
case editId(String)
|
|
case fetchedABContacts(AddressBookContacts, Bool)
|
|
case fetchABContactsRequested
|
|
case onAppear
|
|
case checkDuplicates
|
|
case saveButtonTapped
|
|
case scanButtonTapped
|
|
case walletAccountTapped(WalletAccount)
|
|
}
|
|
|
|
public init() { }
|
|
|
|
@Dependency(\.addressBook) var addressBook
|
|
@Dependency(\.audioServices) var audioServices
|
|
@Dependency(\.derivationTool) var derivationTool
|
|
@Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment
|
|
|
|
public var body: some Reducer<State, Action> {
|
|
BindingReducer()
|
|
|
|
Reduce { state, action in
|
|
switch action {
|
|
case .onAppear:
|
|
state.isValidForm = false
|
|
state.deleteIdToConfirm = nil
|
|
state.nameAlreadyExists = false
|
|
state.addressAlreadyExists = false
|
|
if let editId = state.editId {
|
|
return .concatenate(
|
|
.send(.editId(editId)),
|
|
.send(.fetchABContactsRequested)
|
|
)
|
|
}
|
|
return .send(.fetchABContactsRequested)
|
|
|
|
case .alert(.presented(let action)):
|
|
return .send(action)
|
|
|
|
case .alert(.dismiss):
|
|
state.alert = nil
|
|
return .none
|
|
|
|
case .alert:
|
|
return .none
|
|
|
|
case .addManualButtonTapped:
|
|
state.address = ""
|
|
state.name = ""
|
|
state.isValidForm = false
|
|
state.isAddressFocused = true
|
|
return .none
|
|
|
|
case .checkDuplicates:
|
|
state.nameAlreadyExists = false
|
|
state.addressAlreadyExists = false
|
|
for contact in state.addressBookContacts.contacts {
|
|
if contact.name == state.name && state.name != state.originalName {
|
|
state.nameAlreadyExists = true
|
|
}
|
|
if contact.id == state.address && state.address != state.originalAddress {
|
|
state.addressAlreadyExists = true
|
|
}
|
|
}
|
|
return .none
|
|
|
|
case .scanButtonTapped:
|
|
return .none
|
|
|
|
case .binding:
|
|
state.isValidZcashAddress = derivationTool.isZcashAddress(state.address, zcashSDKEnvironment.network.networkType)
|
|
state.isValidForm = !state.name.isEmpty && state.isValidZcashAddress
|
|
return .send(.checkDuplicates)
|
|
|
|
case .deleteId(let id):
|
|
state.deleteIdToConfirm = id
|
|
state.alert = AlertState.confirmDelete()
|
|
return .none
|
|
|
|
case .deleteIdConfirmed:
|
|
state.alert = nil
|
|
guard let deleteIdToConfirm = state.deleteIdToConfirm, let account = state.zashiWalletAccount else {
|
|
return .none
|
|
}
|
|
|
|
let contact = state.addressBookContacts.contacts.first {
|
|
$0.id == deleteIdToConfirm
|
|
}
|
|
if let contact {
|
|
do {
|
|
let result = try addressBook.deleteContact(account.account, contact)
|
|
let abContacts = result.contacts
|
|
if result.remoteStoreResult == .failure {
|
|
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
|
|
}
|
|
return .concatenate(
|
|
.send(.fetchedABContacts(abContacts, false)),
|
|
.send(.dismissDeleteContactRequired)
|
|
)
|
|
} catch {
|
|
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
|
|
}
|
|
}
|
|
return .none
|
|
|
|
case .walletAccountTapped:
|
|
return .none
|
|
|
|
case .editId(let id):
|
|
guard !state.isInSelectMode else {
|
|
return .none
|
|
}
|
|
|
|
let record = state.addressBookContacts.contacts.first {
|
|
$0.id == id
|
|
}
|
|
guard let record else {
|
|
return .none
|
|
}
|
|
state.address = record.id
|
|
state.name = record.name
|
|
state.originalAddress = state.address
|
|
state.originalName = state.name
|
|
state.isValidForm = true
|
|
state.isNameFocused = true
|
|
state.isValidZcashAddress = derivationTool.isZcashAddress(state.address, zcashSDKEnvironment.network.networkType)
|
|
return .none
|
|
|
|
case .saveButtonTapped:
|
|
guard let account = state.zashiWalletAccount else {
|
|
return .none
|
|
}
|
|
do {
|
|
let result = try addressBook.storeContact(account.account, Contact(address: state.address, name: state.name))
|
|
let abContacts = result.contacts
|
|
if result.remoteStoreResult == .failure {
|
|
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
|
|
}
|
|
return .concatenate(
|
|
.send(.fetchedABContacts(abContacts, false)),
|
|
.send(.contactStoreSuccess),
|
|
.send(.dismissAddContactRequired)
|
|
)
|
|
} catch {
|
|
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
|
|
}
|
|
return .none
|
|
|
|
case .contactStoreSuccess:
|
|
state.address = ""
|
|
state.name = ""
|
|
state.isAddressFocused = false
|
|
state.isNameFocused = false
|
|
return .none
|
|
|
|
case .fetchABContactsRequested:
|
|
guard let account = state.zashiWalletAccount else {
|
|
return .none
|
|
}
|
|
do {
|
|
let result = try addressBook.allLocalContacts(account.account)
|
|
let abContacts = result.contacts
|
|
if result.remoteStoreResult == .failure {
|
|
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
|
|
}
|
|
return .send(.fetchedABContacts(abContacts, true))
|
|
} catch {
|
|
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
|
|
}
|
|
return .none
|
|
|
|
case let .fetchedABContacts(abContacts, requestToSync):
|
|
guard let account = state.zashiWalletAccount else {
|
|
return .none
|
|
}
|
|
state.$addressBookContacts.withLock { $0 = abContacts }
|
|
if requestToSync {
|
|
return .run { send in
|
|
do {
|
|
let result = try await addressBook.syncContacts(account.account, abContacts)
|
|
let syncedContacts = result.contacts
|
|
if result.remoteStoreResult == .failure {
|
|
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
|
|
}
|
|
await send(.fetchedABContacts(syncedContacts, false))
|
|
} catch {
|
|
// TODO: [#1408] error handling https://github.com/Electric-Coin-Company/zashi-ios/issues/1408
|
|
}
|
|
}
|
|
}
|
|
return .none
|
|
|
|
case .dismissAddContactRequired:
|
|
return .none
|
|
|
|
case .dismissDeleteContactRequired:
|
|
return .none
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Alerts
|
|
|
|
extension AlertState where Action == AddressBook.Action {
|
|
public static func confirmDelete() -> AlertState {
|
|
AlertState {
|
|
TextState(L10n.AddressBook.Alert.title)
|
|
} actions: {
|
|
ButtonState(role: .destructive, action: .deleteIdConfirmed) {
|
|
TextState(L10n.General.confirm)
|
|
}
|
|
ButtonState(role: .cancel, action: .alert(.dismiss)) {
|
|
TextState(L10n.General.cancel)
|
|
}
|
|
} message: {
|
|
TextState(L10n.AddressBook.Alert.message)
|
|
}
|
|
}
|
|
}
|