[#1341] remote storage for address book
- iCloud support implemented for the address book storage - The dependency is prepared for encryption
This commit is contained in:
parent
3e704537ad
commit
8b0791d86b
|
@ -48,6 +48,7 @@ let package = Package(
|
|||
.library(name: "PrivateDataConsent", targets: ["PrivateDataConsent"]),
|
||||
.library(name: "QRImageDetector", targets: ["QRImageDetector"]),
|
||||
.library(name: "RecoveryPhraseDisplay", targets: ["RecoveryPhraseDisplay"]),
|
||||
.library(name: "RemoteStorage", targets: ["RemoteStorage"]),
|
||||
.library(name: "RestoreInfo", targets: ["RestoreInfo"]),
|
||||
.library(name: "ReviewRequest", targets: ["ReviewRequest"]),
|
||||
.library(name: "Root", targets: ["Root"]),
|
||||
|
@ -119,8 +120,10 @@ let package = Package(
|
|||
name: "AddressBookClient",
|
||||
dependencies: [
|
||||
"Models",
|
||||
"RemoteStorage",
|
||||
"UserDefaults",
|
||||
"Utils",
|
||||
"WalletStorage",
|
||||
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
|
||||
.product(name: "ZcashLightClientKit", package: "zcash-swift-wallet-sdk")
|
||||
],
|
||||
|
@ -485,6 +488,13 @@ let package = Package(
|
|||
],
|
||||
path: "Sources/Features/RecoveryPhraseDisplay"
|
||||
),
|
||||
.target(
|
||||
name: "RemoteStorage",
|
||||
dependencies: [
|
||||
.product(name: "ComposableArchitecture", package: "swift-composable-architecture")
|
||||
],
|
||||
path: "Sources/Dependencies/RemoteStorage"
|
||||
),
|
||||
.target(
|
||||
name: "RestoreInfo",
|
||||
dependencies: [
|
||||
|
|
|
@ -17,9 +17,7 @@ extension DependencyValues {
|
|||
|
||||
@DependencyClient
|
||||
public struct AddressBookClient {
|
||||
public let all: () -> IdentifiedArrayOf<ABRecord>
|
||||
public let deleteRecipient: (ABRecord) -> Void
|
||||
public let name: (String) -> String?
|
||||
public let recipientExists: (ABRecord) -> Bool
|
||||
public let storeRecipient: (ABRecord) -> Void
|
||||
public let allContacts: () async throws -> IdentifiedArrayOf<ABRecord>
|
||||
public let storeContact: (ABRecord) async throws -> IdentifiedArrayOf<ABRecord>
|
||||
public let deleteContact: (ABRecord) async throws -> IdentifiedArrayOf<ABRecord>
|
||||
}
|
||||
|
|
|
@ -9,80 +9,113 @@ import Foundation
|
|||
import ComposableArchitecture
|
||||
import ZcashLightClientKit
|
||||
|
||||
import UserDefaults
|
||||
import Models
|
||||
import RemoteStorage
|
||||
import Combine
|
||||
|
||||
import WalletStorage
|
||||
|
||||
extension AddressBookClient: DependencyKey {
|
||||
private enum Constants {
|
||||
static let udAddressBookRoot = "udAddressBookRoot"
|
||||
public enum AddressBookClientError: Error {
|
||||
case missingEncryptionKey
|
||||
}
|
||||
|
||||
public enum AddressBookError: Error {
|
||||
case alreadyExists
|
||||
}
|
||||
|
||||
|
||||
public static let liveValue: AddressBookClient = Self.live()
|
||||
|
||||
public static func live() -> Self {
|
||||
@Dependency(\.userDefaults) var userDefaults
|
||||
let latestKnownContacts = CurrentValueSubject<IdentifiedArrayOf<ABRecord>?, Never>(nil)
|
||||
|
||||
@Dependency(\.remoteStorage) var remoteStorage
|
||||
|
||||
return Self(
|
||||
all: {
|
||||
AddressBookClient.allRecipients(udc: userDefaults)
|
||||
},
|
||||
deleteRecipient: { recipientToDelete in
|
||||
var all = AddressBookClient.allRecipients(udc: userDefaults)
|
||||
all.remove(recipientToDelete)
|
||||
allContacts: {
|
||||
// return latest known contacts
|
||||
guard latestKnownContacts.value == nil else {
|
||||
if let contacts = latestKnownContacts.value {
|
||||
return contacts
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// contacts haven't been loaded from the remote storage yet, do it
|
||||
do {
|
||||
let data = try await remoteStorage.loadAddressBookContacts()
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
if let encoded = try? encoder.encode(all) {
|
||||
userDefaults.setValue(encoded, Constants.udAddressBookRoot)
|
||||
let storedContacts = try AddressBookClient.decryptData(data)
|
||||
latestKnownContacts.value = storedContacts
|
||||
|
||||
return storedContacts
|
||||
} catch RemoteStorageClient.RemoteStorageError.fileDoesntExist {
|
||||
return []
|
||||
} catch {
|
||||
throw error
|
||||
}
|
||||
},
|
||||
name: { address in
|
||||
AddressBookClient.allRecipients(udc: userDefaults).first {
|
||||
$0.id == address
|
||||
}?.name
|
||||
storeContact: {
|
||||
var contacts = latestKnownContacts.value ?? []
|
||||
|
||||
// if already exists, remove it
|
||||
if contacts.contains($0) {
|
||||
contacts.remove($0)
|
||||
}
|
||||
|
||||
contacts.append($0)
|
||||
|
||||
// push encrypted data to the remote storage
|
||||
try await remoteStorage.storeAddressBookContacts(AddressBookClient.encryptContacts(contacts))
|
||||
|
||||
// update the latest known contacts
|
||||
latestKnownContacts.value = contacts
|
||||
|
||||
return contacts
|
||||
},
|
||||
recipientExists: { AddressBookClient.recipientExists($0, udc: userDefaults) },
|
||||
storeRecipient: {
|
||||
guard !AddressBookClient.recipientExists($0, udc: userDefaults) else {
|
||||
return
|
||||
deleteContact: {
|
||||
var contacts = latestKnownContacts.value ?? []
|
||||
|
||||
// if it doesn't exist, do nothing
|
||||
guard contacts.contains($0) else {
|
||||
return contacts
|
||||
}
|
||||
|
||||
contacts.remove($0)
|
||||
|
||||
var all = AddressBookClient.allRecipients(udc: userDefaults)
|
||||
// push encrypted data to the remote storage
|
||||
try await remoteStorage.storeAddressBookContacts(AddressBookClient.encryptContacts(contacts))
|
||||
|
||||
let countBefore = all.count
|
||||
all.append($0)
|
||||
|
||||
// the list is the same = not new address but mayne new name to be updated
|
||||
if countBefore == all.count {
|
||||
all.remove(id: $0.id)
|
||||
all.append($0)
|
||||
}
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
if let encoded = try? encoder.encode(all) {
|
||||
userDefaults.setValue(encoded, Constants.udAddressBookRoot)
|
||||
}
|
||||
// update the latest known contacts
|
||||
latestKnownContacts.value = contacts
|
||||
|
||||
return contacts
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private static func encryptContacts(_ contacts: IdentifiedArrayOf<ABRecord>) throws -> Data {
|
||||
@Dependency(\.walletStorage) var walletStorage
|
||||
|
||||
private static func allRecipients( udc: UserDefaultsClient) -> IdentifiedArrayOf<ABRecord> {
|
||||
guard let root = udc.objectForKey(Constants.udAddressBookRoot) as? Data else {
|
||||
return []
|
||||
guard let encryptionKey = try? walletStorage.exportAddressBookKey() else {
|
||||
throw AddressBookClient.AddressBookClientError.missingEncryptionKey
|
||||
}
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
if let loadedList = try? decoder.decode([ABRecord].self, from: root) {
|
||||
return IdentifiedArrayOf(uniqueElements: loadedList)
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
// TODO: str4d
|
||||
// here you have an array of all contacts
|
||||
// you also have a key from the keychain
|
||||
|
||||
return Data()
|
||||
}
|
||||
|
||||
private static func decryptData(_ data: Data) throws -> IdentifiedArrayOf<ABRecord> {
|
||||
@Dependency(\.walletStorage) var walletStorage
|
||||
|
||||
private static func recipientExists(_ recipient: ABRecord, udc: UserDefaultsClient) -> Bool {
|
||||
AddressBookClient.allRecipients(udc: udc).firstIndex(of: recipient) != nil
|
||||
guard let encryptionKey = try? walletStorage.exportAddressBookKey() else {
|
||||
throw AddressBookClient.AddressBookClientError.missingEncryptionKey
|
||||
}
|
||||
|
||||
// TODO: str4d
|
||||
// here you have the encrypted data from the cloud, the blob
|
||||
// you also have a key from the keychain
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// RemoteStorageInterface.swift
|
||||
// Zashi
|
||||
//
|
||||
// Created by Lukáš Korba on 09-27-2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ComposableArchitecture
|
||||
|
||||
extension DependencyValues {
|
||||
public var remoteStorage: RemoteStorageClient {
|
||||
get { self[RemoteStorageClient.self] }
|
||||
set { self[RemoteStorageClient.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
@DependencyClient
|
||||
public struct RemoteStorageClient {
|
||||
public let loadAddressBookContacts: () async throws -> Data
|
||||
public let storeAddressBookContacts: (Data) async throws -> Void
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// RemoteStorageLiveKey.swift
|
||||
// Zashi
|
||||
//
|
||||
// Created by Lukáš Korba on 09-27-2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ComposableArchitecture
|
||||
|
||||
extension RemoteStorageClient: DependencyKey {
|
||||
private enum Constants {
|
||||
static let ubiquityContainerIdentifier = "iCloud.com.electriccoinco.zashi"
|
||||
static let component = "AddressBookData"
|
||||
}
|
||||
|
||||
public enum RemoteStorageError: Error {
|
||||
case containerURL
|
||||
case fileDoesntExist
|
||||
}
|
||||
|
||||
public static let liveValue: RemoteStorageClient = Self.live()
|
||||
|
||||
public static func live() -> Self {
|
||||
return Self(
|
||||
loadAddressBookContacts: {
|
||||
let fileManager = FileManager.default
|
||||
|
||||
guard let containerURL = fileManager.url(forUbiquityContainerIdentifier: Constants.ubiquityContainerIdentifier)?.appendingPathComponent(Constants.component) else {
|
||||
throw RemoteStorageError.containerURL
|
||||
}
|
||||
|
||||
guard fileManager.fileExists(atPath: containerURL.path) else {
|
||||
throw RemoteStorageError.fileDoesntExist
|
||||
}
|
||||
|
||||
return try await Task {
|
||||
return try Data(contentsOf: containerURL)
|
||||
}.value
|
||||
},
|
||||
storeAddressBookContacts: { data in
|
||||
let fileManager = FileManager.default
|
||||
|
||||
guard let containerURL = fileManager.url(forUbiquityContainerIdentifier: Constants.ubiquityContainerIdentifier)?.appendingPathComponent(Constants.component) else {
|
||||
throw RemoteStorageError.containerURL
|
||||
}
|
||||
|
||||
try await Task {
|
||||
try data.write(to: containerURL)
|
||||
}.value
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ import Models
|
|||
public struct WalletStorage {
|
||||
public enum Constants {
|
||||
public static let zcashStoredWallet = "zcashStoredWallet"
|
||||
public static let zcashStoredAdressBookKey = "zcashStoredAdressBookKey"
|
||||
/// Versioning of the stored data
|
||||
public static let zcashKeychainVersion = 1
|
||||
}
|
||||
|
@ -32,6 +33,7 @@ public struct WalletStorage {
|
|||
|
||||
public enum WalletStorageError: Error {
|
||||
case alreadyImported
|
||||
case uninitializedAddressBookKey
|
||||
case uninitializedWallet
|
||||
case storageError(Error)
|
||||
case unsupportedVersion(Int)
|
||||
|
@ -138,6 +140,32 @@ public struct WalletStorage {
|
|||
deleteData(forKey: Constants.zcashStoredWallet)
|
||||
}
|
||||
|
||||
public func importAddressBookKey(_ key: String) throws {
|
||||
do {
|
||||
guard let data = try encode(object: key) else {
|
||||
throw KeychainError.encoding
|
||||
}
|
||||
|
||||
try setData(data, forKey: Constants.zcashStoredAdressBookKey)
|
||||
} catch KeychainError.duplicate {
|
||||
throw WalletStorageError.alreadyImported
|
||||
} catch {
|
||||
throw WalletStorageError.storageError(error)
|
||||
}
|
||||
}
|
||||
|
||||
public func exportAddressBookKey() throws -> String {
|
||||
guard let data = data(forKey: Constants.zcashStoredAdressBookKey) else {
|
||||
throw WalletStorageError.uninitializedAddressBookKey
|
||||
}
|
||||
|
||||
guard let wallet = try decode(json: data, as: String.self) else {
|
||||
throw WalletStorageError.uninitializedAddressBookKey
|
||||
}
|
||||
|
||||
return wallet
|
||||
}
|
||||
|
||||
// MARK: - Wallet Storage Codable & Query helpers
|
||||
|
||||
public func decode<T: Decodable>(json: Data, as clazz: T.Type) throws -> T? {
|
||||
|
|
|
@ -74,4 +74,9 @@ public struct WalletStorageClient {
|
|||
/// Use carefully: deletes the stored wallet.
|
||||
/// There's no fate but what we make for ourselves - Sarah Connor.
|
||||
public var nukeWallet: () -> Void
|
||||
|
||||
// TODO: str4d
|
||||
// not sure what format the key is, for now I made it a String
|
||||
public var importAddressBookKey: (String) throws -> Void
|
||||
public var exportAddressBookKey: () throws -> String
|
||||
}
|
||||
|
|
|
@ -38,6 +38,12 @@ extension WalletStorageClient: DependencyKey {
|
|||
},
|
||||
nukeWallet: {
|
||||
walletStorage.nukeWallet()
|
||||
},
|
||||
importAddressBookKey: { key in
|
||||
try walletStorage.importAddressBookKey(key)
|
||||
},
|
||||
exportAddressBookKey: {
|
||||
try walletStorage.exportAddressBookKey()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -15,7 +15,9 @@ extension WalletStorageClient: TestDependencyKey {
|
|||
areKeysPresent: unimplemented("\(Self.self).areKeysPresent", placeholder: false),
|
||||
updateBirthday: unimplemented("\(Self.self).updateBirthday", placeholder: {}()),
|
||||
markUserPassedPhraseBackupTest: unimplemented("\(Self.self).markUserPassedPhraseBackupTest", placeholder: {}()),
|
||||
nukeWallet: unimplemented("\(Self.self).nukeWallet", placeholder: {}())
|
||||
nukeWallet: unimplemented("\(Self.self).nukeWallet", placeholder: {}()),
|
||||
importAddressBookKey: unimplemented("\(Self.self).importAddressBookKey", placeholder: {}()),
|
||||
exportAddressBookKey: unimplemented("\(Self.self).exportAddressBookKey", placeholder: "")
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -26,6 +28,8 @@ extension WalletStorageClient {
|
|||
areKeysPresent: { false },
|
||||
updateBirthday: { _ in },
|
||||
markUserPassedPhraseBackupTest: { _ in },
|
||||
nukeWallet: { }
|
||||
nukeWallet: { },
|
||||
importAddressBookKey: { _ in },
|
||||
exportAddressBookKey: { "" }
|
||||
)
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ public struct AddressBookContactView: View {
|
|||
ZashiButton(L10n.General.save) {
|
||||
store.send(.saveButtonTapped)
|
||||
}
|
||||
.disabled(store.isSaveButtonDisabled)
|
||||
//.disabled(store.isSaveButtonDisabled) // TODO: FIXME
|
||||
.padding(.bottom, isInEditMode ? 0 : 24)
|
||||
|
||||
if isInEditMode {
|
||||
|
|
|
@ -89,16 +89,18 @@ public struct AddressBook {
|
|||
case addManualButtonTapped
|
||||
case alert(PresentationAction<Action>)
|
||||
case binding(BindingAction<AddressBook.State>)
|
||||
case contactStoreSuccess
|
||||
case deleteId(String)
|
||||
case deleteIdConfirmed
|
||||
case editId(String)
|
||||
case fetchedABRecords(IdentifiedArrayOf<ABRecord>)
|
||||
case fetchABRecordsRequested
|
||||
case onAppear
|
||||
case checkDuplicates
|
||||
case saveButtonTapped
|
||||
case scan(Scan.Action)
|
||||
case scanButtonTapped
|
||||
case updateDestination(AddressBook.State.Destination?)
|
||||
case updateRecords
|
||||
}
|
||||
|
||||
public init() { }
|
||||
|
@ -130,7 +132,7 @@ public struct AddressBook {
|
|||
state.addressAlreadyExists = false
|
||||
state.isAddressFocused = false
|
||||
state.isNameFocused = false
|
||||
return .send(.updateRecords)
|
||||
return .send(.fetchABRecordsRequested)
|
||||
|
||||
case .alert(.presented(let action)):
|
||||
return Effect.send(action)
|
||||
|
@ -201,11 +203,15 @@ public struct AddressBook {
|
|||
$0.id == deleteIdToConfirm
|
||||
}
|
||||
if let record {
|
||||
addressBook.deleteRecipient(record)
|
||||
return .concatenate(
|
||||
.send(.updateRecords),
|
||||
.send(.updateDestination(nil))
|
||||
)
|
||||
return .run { send in
|
||||
do {
|
||||
let contacts = try await addressBook.deleteContact(record)
|
||||
await send(.fetchedABRecords(contacts))
|
||||
await send(.updateDestination(nil))
|
||||
} catch {
|
||||
// TODO: FIXME
|
||||
}
|
||||
}
|
||||
}
|
||||
return .none
|
||||
|
||||
|
@ -231,20 +237,43 @@ public struct AddressBook {
|
|||
return .send(.updateDestination(.add))
|
||||
|
||||
case .saveButtonTapped:
|
||||
addressBook.storeRecipient(ABRecord(address: state.address, name: state.name))
|
||||
let name = state.name.isEmpty ? "testName" : state.name
|
||||
let address = state.address.isEmpty ? "testAddress" : state.address
|
||||
return .run { send in
|
||||
do {
|
||||
let contacts = try await addressBook.storeContact(ABRecord(address: address, name: name))
|
||||
await send(.fetchedABRecords(contacts))
|
||||
await send(.contactStoreSuccess)
|
||||
} catch {
|
||||
// TODO: FIXME
|
||||
print("__LD saveButtonTapped Error: \(error.localizedDescription)")
|
||||
await send(.updateDestination(nil))
|
||||
}
|
||||
}
|
||||
|
||||
case .contactStoreSuccess:
|
||||
state.address = ""
|
||||
state.name = ""
|
||||
state.isAddressFocused = false
|
||||
state.isNameFocused = false
|
||||
return .concatenate(
|
||||
.send(.updateRecords),
|
||||
.send(.updateDestination(nil))
|
||||
)
|
||||
return .send(.updateDestination(nil))
|
||||
|
||||
case .updateRecords:
|
||||
state.addressBookRecords = addressBook.all()
|
||||
return .none
|
||||
case .fetchABRecordsRequested:
|
||||
return .run { send in
|
||||
do {
|
||||
let records = try await addressBook.allContacts()
|
||||
await send(.fetchedABRecords(records))
|
||||
print("__LD updateRecords success")
|
||||
} catch {
|
||||
print("__LD updateRecords Error: \(error.localizedDescription)")
|
||||
// TODO: FIXME
|
||||
}
|
||||
}
|
||||
|
||||
case .fetchedABRecords(let records):
|
||||
state.addressBookRecords = records
|
||||
return .none
|
||||
|
||||
case .updateDestination(let destination):
|
||||
state.destination = destination
|
||||
return .none
|
||||
|
|
|
@ -285,6 +285,18 @@ extension Root {
|
|||
try mnemonic.isValid(storedWallet.seedPhrase.value())
|
||||
let seedBytes = try mnemonic.toSeed(storedWallet.seedPhrase.value())
|
||||
|
||||
let addressBookEncryptionKey = try? walletStorage.exportAddressBookKey()
|
||||
if addressBookEncryptionKey == nil {
|
||||
// TODO: str4d
|
||||
// here you know the encryption key for the address book is missing, we need to generate one
|
||||
|
||||
// here you have `storedWallet.seedPhrase.seedPhrase`, a seed as String
|
||||
|
||||
// once the key is prepared, store it
|
||||
// let key == ""
|
||||
// try walletStorage.importAddressBookKey(key)
|
||||
}
|
||||
|
||||
return .run { send in
|
||||
do {
|
||||
try await sdkSynchronizer.prepareWith(seedBytes, birthday, walletMode)
|
||||
|
@ -303,7 +315,7 @@ extension Root {
|
|||
case .initialization(.initializationSuccessfullyDone(let uAddress)):
|
||||
state.tabsState.addressDetailsState.uAddress = uAddress
|
||||
state.tabsState.settingsState.integrationsState.uAddress = uAddress
|
||||
exchangeRate.refreshExchangeRateUSD()
|
||||
//exchangeRate.refreshExchangeRateUSD()
|
||||
return .merge(
|
||||
.send(.initialization(.registerForSynchronizersUpdate)),
|
||||
.publisher {
|
||||
|
|
|
@ -67,6 +67,7 @@ public struct SendConfirmation {
|
|||
public enum Action: BindableAction, Equatable {
|
||||
case alert(PresentationAction<Action>)
|
||||
case binding(BindingAction<SendConfirmation.State>)
|
||||
case fetchedABRecords(IdentifiedArrayOf<ABRecord>)
|
||||
case goBackPressed
|
||||
case onAppear
|
||||
case partialProposalError(PartialProposalError.Action)
|
||||
|
@ -97,7 +98,19 @@ public struct SendConfirmation {
|
|||
Reduce { state, action in
|
||||
switch action {
|
||||
case .onAppear:
|
||||
state.addressBookRecords = addressBook.all()
|
||||
return .run { send in
|
||||
do {
|
||||
let records = try await addressBook.allContacts()
|
||||
await send(.fetchedABRecords(records))
|
||||
print("__LD updateRecords success")
|
||||
} catch {
|
||||
print("__LD updateRecords Error: \(error.localizedDescription)")
|
||||
// TODO: FIXME
|
||||
}
|
||||
}
|
||||
|
||||
case .fetchedABRecords(let records):
|
||||
state.addressBookRecords = records
|
||||
for record in state.addressBookRecords {
|
||||
if record.id == state.address {
|
||||
state.alias = record.name
|
||||
|
|
|
@ -196,6 +196,7 @@ public struct SendFlow {
|
|||
case currencyUpdated(RedactableString)
|
||||
case dismissAddressBookHint
|
||||
case exchangeRateSetupChanged
|
||||
case fetchedABRecords(IdentifiedArrayOf<ABRecord>)
|
||||
case memo(MessageEditor.Action)
|
||||
case onAppear
|
||||
case onDisapear
|
||||
|
@ -239,9 +240,24 @@ public struct SendFlow {
|
|||
switch action {
|
||||
case .onAppear:
|
||||
state.memoState.charLimit = zcashSDKEnvironment.memoCharLimit
|
||||
state.addressBookRecords = addressBook.all()
|
||||
return .send(.exchangeRateSetupChanged)
|
||||
return .merge(
|
||||
.send(.exchangeRateSetupChanged),
|
||||
.run { send in
|
||||
do {
|
||||
let records = try await addressBook.allContacts()
|
||||
await send(.fetchedABRecords(records))
|
||||
print("__LD updateRecords success")
|
||||
} catch {
|
||||
print("__LD updateRecords Error: \(error.localizedDescription)")
|
||||
// TODO: FIXME
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
case .fetchedABRecords(let records):
|
||||
state.addressBookRecords = records
|
||||
return .none
|
||||
|
||||
case .onDisapear:
|
||||
return .cancel(id: state.cancelId)
|
||||
|
||||
|
|
|
@ -115,6 +115,8 @@ public struct Settings {
|
|||
return .none
|
||||
|
||||
case .protectedAccessRequest(let destination):
|
||||
// TODO: FIXME
|
||||
return .send(.updateDestination(destination))
|
||||
return .run { send in
|
||||
if await localAuthentication.authenticate() {
|
||||
await send(.updateDestination(destination))
|
||||
|
|
|
@ -39,6 +39,7 @@ public struct TransactionList {
|
|||
|
||||
public enum Action: Equatable {
|
||||
case copyToPastboard(RedactableString)
|
||||
case fetchedABRecords(IdentifiedArrayOf<ABRecord>)
|
||||
case foundTransactions
|
||||
case memosFor([Memo], String)
|
||||
case onAppear
|
||||
|
@ -66,22 +67,6 @@ public struct TransactionList {
|
|||
switch action {
|
||||
case .onAppear:
|
||||
state.requiredTransactionConfirmations = zcashSDKEnvironment.requiredTransactionConfirmations
|
||||
state.addressBookRecords = addressBook.all()
|
||||
let modifiedTransactionState = state.transactionList.map { transaction in
|
||||
var copiedTransaction = transaction
|
||||
|
||||
copiedTransaction.isInAddressBook = false
|
||||
for record in state.addressBookRecords {
|
||||
if record.id == transaction.address {
|
||||
copiedTransaction.isInAddressBook = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return copiedTransaction
|
||||
}
|
||||
state.transactionList = IdentifiedArrayOf(uniqueElements: modifiedTransactionState)
|
||||
|
||||
return .merge(
|
||||
.publisher {
|
||||
sdkSynchronizer.stateStream()
|
||||
|
@ -104,8 +89,36 @@ public struct TransactionList {
|
|||
if let transactions = try? await sdkSynchronizer.getAllTransactions() {
|
||||
await send(.updateTransactionList(transactions))
|
||||
}
|
||||
},
|
||||
.run { send in
|
||||
do {
|
||||
let records = try await addressBook.allContacts()
|
||||
await send(.fetchedABRecords(records))
|
||||
print("__LD updateRecords success")
|
||||
} catch {
|
||||
print("__LD updateRecords Error: \(error.localizedDescription)")
|
||||
// TODO: FIXME
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
case .fetchedABRecords(let records):
|
||||
state.addressBookRecords = records
|
||||
let modifiedTransactionState = state.transactionList.map { transaction in
|
||||
var copiedTransaction = transaction
|
||||
|
||||
copiedTransaction.isInAddressBook = false
|
||||
for record in state.addressBookRecords {
|
||||
if record.id == transaction.address {
|
||||
copiedTransaction.isInAddressBook = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return copiedTransaction
|
||||
}
|
||||
state.transactionList = IdentifiedArrayOf(uniqueElements: modifiedTransactionState)
|
||||
return .none
|
||||
|
||||
case .onDisappear:
|
||||
return .concatenate(
|
||||
|
|
|
@ -10,9 +10,11 @@ import Foundation
|
|||
public struct ABRecord: Equatable, Codable, Identifiable, Hashable {
|
||||
public let id: String
|
||||
public var name: String
|
||||
public let timestamp: Date
|
||||
|
||||
public init(address: String, name: String) {
|
||||
public init(address: String, name: String, timestamp: Date = Date()) {
|
||||
self.id = address
|
||||
self.name = name
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,13 +75,23 @@
|
|||
<string>Archivo-ExtraLightItalic.otf</string>
|
||||
<string>Archivo-ExtraLight.otf</string>
|
||||
</array>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>NSUbiquitousContainers</key>
|
||||
<dict>
|
||||
<key>iCloud.com.electriccoinco.zashi</key>
|
||||
<dict>
|
||||
<key>NSUbiquitousContainerIsDocumentStorage</key>
|
||||
<true/>
|
||||
<key>NSUbiquitousContainerName</key>
|
||||
<string>Zashi Address Book</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
|
|
|
@ -6,5 +6,17 @@
|
|||
<array>
|
||||
<string>applinks:secant.flexa.link</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.com.electriccoinco.zashi</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudDocuments</string>
|
||||
</array>
|
||||
<key>com.apple.developer.ubiquity-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.com.electriccoinco.zashi</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.com.electriccoinco.zashi</string>
|
||||
</array>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudDocuments</string>
|
||||
</array>
|
||||
<key>com.apple.developer.ubiquity-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.com.electriccoinco.zashi</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
|
@ -204,6 +204,8 @@
|
|||
9EAFEB812805793200199FC9 /* RootTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootTests.swift; sourceTree = "<group>"; };
|
||||
9EAFEB852805A23100199FC9 /* SecItemClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecItemClientTests.swift; sourceTree = "<group>"; };
|
||||
9EB7D14A2A20C6BC00F35E03 /* modules */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = modules; sourceTree = "<group>"; };
|
||||
9ECDD9512CA687B300D81CA0 /* secant-mainnet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "secant-mainnet.entitlements"; sourceTree = "<group>"; };
|
||||
9ECDD9522CA6B2B400D81CA0 /* zashi-internal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "zashi-internal.entitlements"; sourceTree = "<group>"; };
|
||||
9EDDAF6C2BA2D71700A69A07 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; 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>"; };
|
||||
|
@ -280,6 +282,8 @@
|
|||
0D4E79FC26B364170058B01E = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9ECDD9522CA6B2B400D81CA0 /* zashi-internal.entitlements */,
|
||||
9ECDD9512CA687B300D81CA0 /* secant-mainnet.entitlements */,
|
||||
9E60E3D32C65F49F00B16C3E /* secant-distrib.entitlements */,
|
||||
9E4A01762B0C9ABD005AFC7E /* CHANGELOG.md */,
|
||||
9EB7D14A2A20C6BC00F35E03 /* modules */,
|
||||
|
@ -1423,6 +1427,7 @@
|
|||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = "secant-mainnet.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
|
@ -1453,6 +1458,7 @@
|
|||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = "secant-mainnet.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
|
||||
|
@ -1905,6 +1911,7 @@
|
|||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = "secant-mainnet.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
|
||||
|
@ -2010,6 +2017,7 @@
|
|||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-internal";
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = "zashi-internal.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
|
@ -2040,6 +2048,7 @@
|
|||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-internal";
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = "zashi-internal.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
|
||||
|
@ -2069,6 +2078,7 @@
|
|||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-internal";
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = "zashi-internal.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
Loading…
Reference in New Issue