[#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:
Lukas Korba 2024-09-27 13:44:14 +02:00
parent 3e704537ad
commit 8b0791d86b
22 changed files with 399 additions and 97 deletions

View File

@ -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: [

View File

@ -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>
}

View File

@ -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 []
}
}

View File

@ -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
}

View File

@ -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
}
)
}
}

View File

@ -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? {

View File

@ -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
}

View File

@ -38,6 +38,12 @@ extension WalletStorageClient: DependencyKey {
},
nukeWallet: {
walletStorage.nukeWallet()
},
importAddressBookKey: { key in
try walletStorage.importAddressBookKey(key)
},
exportAddressBookKey: {
try walletStorage.exportAddressBookKey()
}
)
}

View File

@ -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: { "" }
)
}

View File

@ -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 {

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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)

View File

@ -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))

View File

@ -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(

View File

@ -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
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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\"";

View File

@ -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>