Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • mobile/ios/elixxir-dapps-sdk-swift
1 result
Show changes
Commits on Source (44)
Showing
with 1356 additions and 71 deletions
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1340"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "ContactFeature"
BuildableName = "ContactFeature"
BlueprintName = "ContactFeature"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "ContactFeatureTests"
BuildableName = "ContactFeatureTests"
BlueprintName = "ContactFeatureTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "ContactFeature"
BuildableName = "ContactFeature"
BlueprintName = "ContactFeature"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1340"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SendRequestFeature"
BuildableName = "SendRequestFeature"
BlueprintName = "SendRequestFeature"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SendRequestFeatureTests"
BuildableName = "SendRequestFeatureTests"
BlueprintName = "SendRequestFeatureTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SendRequestFeature"
BuildableName = "SendRequestFeature"
BlueprintName = "SendRequestFeature"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
......@@ -20,9 +20,11 @@ let package = Package(
products: [
.library(name: "AppCore", targets: ["AppCore"]),
.library(name: "AppFeature", targets: ["AppFeature"]),
.library(name: "ContactFeature", targets: ["ContactFeature"]),
.library(name: "HomeFeature", targets: ["HomeFeature"]),
.library(name: "RegisterFeature", targets: ["RegisterFeature"]),
.library(name: "RestoreFeature", targets: ["RestoreFeature"]),
.library(name: "SendRequestFeature", targets: ["SendRequestFeature"]),
.library(name: "UserSearchFeature", targets: ["UserSearchFeature"]),
.library(name: "WelcomeFeature", targets: ["WelcomeFeature"]),
],
......@@ -52,6 +54,7 @@ let package = Package(
name: "AppCore",
dependencies: [
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
.product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
.product(name: "XXDatabase", package: "client-ios-db"),
.product(name: "XXModels", package: "client-ios-db"),
],
......@@ -68,9 +71,11 @@ let package = Package(
name: "AppFeature",
dependencies: [
.target(name: "AppCore"),
.target(name: "ContactFeature"),
.target(name: "HomeFeature"),
.target(name: "RegisterFeature"),
.target(name: "RestoreFeature"),
.target(name: "SendRequestFeature"),
.target(name: "UserSearchFeature"),
.target(name: "WelcomeFeature"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
......@@ -87,6 +92,25 @@ let package = Package(
],
swiftSettings: swiftSettings
),
.target(
name: "ContactFeature",
dependencies: [
.target(name: "AppCore"),
.target(name: "SendRequestFeature"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "ComposablePresentation", package: "swift-composable-presentation"),
.product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"),
.product(name: "XXModels", package: "client-ios-db"),
],
swiftSettings: swiftSettings
),
.testTarget(
name: "ContactFeatureTests",
dependencies: [
.target(name: "ContactFeature"),
],
swiftSettings: swiftSettings
),
.target(
name: "HomeFeature",
dependencies: [
......@@ -111,6 +135,7 @@ let package = Package(
dependencies: [
.target(name: "AppCore"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
.product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"),
.product(name: "XXModels", package: "client-ios-db"),
],
......@@ -137,12 +162,33 @@ let package = Package(
],
swiftSettings: swiftSettings
),
.target(
name: "SendRequestFeature",
dependencies: [
.target(name: "AppCore"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
.product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"),
.product(name: "XXModels", package: "client-ios-db"),
],
swiftSettings: swiftSettings
),
.testTarget(
name: "SendRequestFeatureTests",
dependencies: [
.target(name: "SendRequestFeature"),
],
swiftSettings: swiftSettings
),
.target(
name: "UserSearchFeature",
dependencies: [
.target(name: "ContactFeature"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "ComposablePresentation", package: "swift-composable-presentation"),
.product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
.product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"),
.product(name: "XXModels", package: "client-ios-db"),
],
swiftSettings: swiftSettings
),
......
......@@ -49,6 +49,16 @@
ReferencedContainer = "container:..">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "ContactFeatureTests"
BuildableName = "ContactFeatureTests"
BlueprintName = "ContactFeatureTests"
ReferencedContainer = "container:..">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
......@@ -79,6 +89,16 @@
ReferencedContainer = "container:..">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SendRequestFeatureTests"
BuildableName = "SendRequestFeatureTests"
BlueprintName = "SendRequestFeatureTests"
ReferencedContainer = "container:..">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
......
......@@ -4,6 +4,7 @@ public struct DBManager {
public var hasDB: DBManagerHasDB
public var makeDB: DBManagerMakeDB
public var getDB: DBManagerGetDB
public var removeDB: DBManagerRemoveDB
}
extension DBManager {
......@@ -17,7 +18,8 @@ extension DBManager {
return DBManager(
hasDB: .init { container.db != nil },
makeDB: .live(setDB: { container.db = $0 }),
getDB: .live(getDB: { container.db })
getDB: .live(getDB: { container.db }),
removeDB: .live(getDB: { container.db }, unsetDB: { container.db = nil })
)
}
}
......@@ -26,6 +28,7 @@ extension DBManager {
public static let unimplemented = DBManager(
hasDB: .unimplemented,
makeDB: .unimplemented,
getDB: .unimplemented
getDB: .unimplemented,
removeDB: .unimplemented
)
}
import Foundation
import XCTestDynamicOverlay
import XXDatabase
import XXModels
public struct DBManagerRemoveDB {
public var run: () throws -> Void
public func callAsFunction() throws -> Void {
try run()
}
}
extension DBManagerRemoveDB {
public static func live(
getDB: @escaping () -> Database?,
unsetDB: @escaping () -> Void
) -> DBManagerRemoveDB {
DBManagerRemoveDB {
try getDB()?.drop()
unsetDB()
}
}
}
extension DBManagerRemoveDB {
public static let unimplemented = DBManagerRemoveDB(
run: XCTUnimplemented("\(Self.self)")
)
}
import AppCore
import ContactFeature
import Foundation
import HomeFeature
import RegisterFeature
import RestoreFeature
import SendRequestFeature
import UserSearchFeature
import WelcomeFeature
import XXMessengerClient
......@@ -34,7 +36,7 @@ extension AppEnvironment {
home: {
HomeEnvironment(
messenger: messenger,
db: dbManager.getDB,
dbManager: dbManager,
mainQueue: mainQueue,
bgQueue: bgQueue,
register: {
......@@ -50,7 +52,26 @@ extension AppEnvironment {
UserSearchEnvironment(
messenger: messenger,
mainQueue: mainQueue,
bgQueue: bgQueue
bgQueue: bgQueue,
result: {
UserSearchResultEnvironment()
},
contact: {
ContactEnvironment(
messenger: messenger,
db: dbManager.getDB,
mainQueue: mainQueue,
bgQueue: bgQueue,
sendRequest: {
SendRequestEnvironment(
messenger: messenger,
db: dbManager.getDB,
mainQueue: mainQueue,
bgQueue: bgQueue
)
}
)
}
)
}
)
......
import AppCore
import ComposableArchitecture
import ComposablePresentation
import Foundation
import SendRequestFeature
import XCTestDynamicOverlay
import XXClient
import XXMessengerClient
import XXModels
public struct ContactState: Equatable {
public init(
id: Data,
dbContact: XXModels.Contact? = nil,
xxContact: XXClient.Contact? = nil,
importUsername: Bool = true,
importEmail: Bool = true,
importPhone: Bool = true,
sendRequest: SendRequestState? = nil
) {
self.id = id
self.dbContact = dbContact
self.xxContact = xxContact
self.importUsername = importUsername
self.importEmail = importEmail
self.importPhone = importPhone
self.sendRequest = sendRequest
}
public var id: Data
public var dbContact: XXModels.Contact?
public var xxContact: XXClient.Contact?
@BindableState public var importUsername: Bool
@BindableState public var importEmail: Bool
@BindableState public var importPhone: Bool
public var sendRequest: SendRequestState?
}
public enum ContactAction: Equatable, BindableAction {
case start
case dbContactFetched(XXModels.Contact?)
case importFactsTapped
case sendRequestTapped
case sendRequestDismissed
case sendRequest(SendRequestAction)
case binding(BindingAction<ContactState>)
}
public struct ContactEnvironment {
public init(
messenger: Messenger,
db: DBManagerGetDB,
mainQueue: AnySchedulerOf<DispatchQueue>,
bgQueue: AnySchedulerOf<DispatchQueue>,
sendRequest: @escaping () -> SendRequestEnvironment
) {
self.messenger = messenger
self.db = db
self.mainQueue = mainQueue
self.bgQueue = bgQueue
self.sendRequest = sendRequest
}
public var messenger: Messenger
public var db: DBManagerGetDB
public var mainQueue: AnySchedulerOf<DispatchQueue>
public var bgQueue: AnySchedulerOf<DispatchQueue>
public var sendRequest: () -> SendRequestEnvironment
}
#if DEBUG
extension ContactEnvironment {
public static let unimplemented = ContactEnvironment(
messenger: .unimplemented,
db: .unimplemented,
mainQueue: .unimplemented,
bgQueue: .unimplemented,
sendRequest: { .unimplemented }
)
}
#endif
public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironment>
{ state, action, env in
enum DBFetchEffectID {}
switch action {
case .start:
return try! env.db().fetchContactsPublisher(.init(id: [state.id]))
.assertNoFailure()
.map(\.first)
.map(ContactAction.dbContactFetched)
.subscribe(on: env.bgQueue)
.receive(on: env.mainQueue)
.eraseToEffect()
.cancellable(id: DBFetchEffectID.self, cancelInFlight: true)
case .dbContactFetched(let contact):
state.dbContact = contact
return .none
case .importFactsTapped:
guard let xxContact = state.xxContact else { return .none }
return .fireAndForget { [state] in
var dbContact = state.dbContact ?? XXModels.Contact(id: state.id)
dbContact.marshaled = xxContact.data
if state.importUsername {
dbContact.username = try? xxContact.getFact(.username)?.fact
}
if state.importEmail {
dbContact.email = try? xxContact.getFact(.email)?.fact
}
if state.importPhone {
dbContact.phone = try? xxContact.getFact(.phone)?.fact
}
_ = try! env.db().saveContact(dbContact)
}
.subscribe(on: env.bgQueue)
.receive(on: env.mainQueue)
.eraseToEffect()
case .sendRequestTapped:
if let xxContact = state.xxContact {
state.sendRequest = SendRequestState(contact: xxContact)
} else if let marshaled = state.dbContact?.marshaled {
state.sendRequest = SendRequestState(contact: .live(marshaled))
}
return .none
case .sendRequestDismissed:
state.sendRequest = nil
return .none
case .sendRequest(.sendSucceeded):
state.sendRequest = nil
return .none
case .sendRequest(_):
return .none
case .binding(_):
return .none
}
}
.binding()
.presenting(
sendRequestReducer,
state: .keyPath(\.sendRequest),
id: .notNil(),
action: /ContactAction.sendRequest,
environment: { $0.sendRequest() }
)
import AppCore
import ComposableArchitecture
import ComposablePresentation
import SendRequestFeature
import SwiftUI
import XXClient
import XXModels
public struct ContactView: View {
public init(store: Store<ContactState, ContactAction>) {
self.store = store
}
let store: Store<ContactState, ContactAction>
struct ViewState: Equatable {
var dbContact: XXModels.Contact?
var xxContactIsSet: Bool
var xxContactUsername: String?
var xxContactEmail: String?
var xxContactPhone: String?
var importUsername: Bool
var importEmail: Bool
var importPhone: Bool
init(state: ContactState) {
dbContact = state.dbContact
xxContactIsSet = state.xxContact != nil
xxContactUsername = try? state.xxContact?.getFact(.username)?.fact
xxContactEmail = try? state.xxContact?.getFact(.email)?.fact
xxContactPhone = try? state.xxContact?.getFact(.phone)?.fact
importUsername = state.importUsername
importEmail = state.importEmail
importPhone = state.importPhone
}
}
public var body: some View {
WithViewStore(store.scope(state: ViewState.init)) { viewStore in
Form {
if viewStore.xxContactIsSet {
Section {
Button {
viewStore.send(.set(\.$importUsername, !viewStore.importUsername))
} label: {
HStack {
Label(viewStore.xxContactUsername ?? "", systemImage: "person")
.tint(Color.primary)
Spacer()
Image(systemName: viewStore.importUsername ? "checkmark.circle.fill" : "circle")
.foregroundColor(.accentColor)
}
}
Button {
viewStore.send(.set(\.$importEmail, !viewStore.importEmail))
} label: {
HStack {
Label(viewStore.xxContactEmail ?? "", systemImage: "envelope")
.tint(Color.primary)
Spacer()
Image(systemName: viewStore.importEmail ? "checkmark.circle.fill" : "circle")
.foregroundColor(.accentColor)
}
}
Button {
viewStore.send(.set(\.$importPhone, !viewStore.importPhone))
} label: {
HStack {
Label(viewStore.xxContactPhone ?? "", systemImage: "phone")
.tint(Color.primary)
Spacer()
Image(systemName: viewStore.importPhone ? "checkmark.circle.fill" : "circle")
.foregroundColor(.accentColor)
}
}
Button {
viewStore.send(.importFactsTapped)
} label: {
if viewStore.dbContact == nil {
Text("Save contact")
} else {
Text("Update contact")
}
}
} header: {
Text("Facts")
}
}
if let dbContact = viewStore.dbContact {
Section {
Label(dbContact.username ?? "", systemImage: "person")
Label(dbContact.email ?? "", systemImage: "envelope")
Label(dbContact.phone ?? "", systemImage: "phone")
} header: {
Text("Contact")
}
Section {
switch dbContact.authStatus {
case .stranger:
HStack {
Text("Stranger")
Spacer()
Image(systemName: "person.fill.questionmark")
}
case .requesting:
HStack {
Text("Sending auth request")
Spacer()
ProgressView()
}
case .requested:
HStack {
Text("Request sent")
Spacer()
Image(systemName: "paperplane")
}
case .requestFailed:
HStack {
Text("Sending request failed")
Spacer()
Image(systemName: "xmark.diamond.fill")
.foregroundColor(.red)
}
case .verificationInProgress:
HStack {
Text("Verification is progress")
Spacer()
ProgressView()
}
case .verified:
HStack {
Text("Verified")
Spacer()
Image(systemName: "person.fill.checkmark")
}
case .verificationFailed:
HStack {
Text("Verification failed")
Spacer()
Image(systemName: "xmark.diamond.fill")
.foregroundColor(.red)
}
case .confirming:
HStack {
Text("Confirming auth request")
Spacer()
ProgressView()
}
case .confirmationFailed:
HStack {
Text("Confirmation failed")
Spacer()
Image(systemName: "xmark.diamond.fill")
.foregroundColor(.red)
}
case .friend:
HStack {
Text("Friend")
Spacer()
Image(systemName: "person.fill.checkmark")
}
case .hidden:
HStack {
Text("Hidden")
Spacer()
Image(systemName: "eye.slash")
}
}
Button {
viewStore.send(.sendRequestTapped)
} label: {
HStack {
Text("Send request")
Spacer()
Image(systemName: "chevron.forward")
}
}
} header: {
Text("Auth")
}
.animation(.default, value: viewStore.dbContact?.authStatus)
}
}
.navigationTitle("Contact")
.task { viewStore.send(.start) }
.background(NavigationLinkWithStore(
store.scope(
state: \.sendRequest,
action: ContactAction.sendRequest
),
mapState: replayNonNil(),
onDeactivate: { viewStore.send(.sendRequestDismissed) },
destination: SendRequestView.init(store:)
))
}
}
}
#if DEBUG
public struct ContactView_Previews: PreviewProvider {
public static var previews: some View {
ContactView(store: Store(
initialState: ContactState(
id: "contact-id".data(using: .utf8)!
),
reducer: .empty,
environment: ()
))
}
}
#endif
......@@ -71,14 +71,14 @@ public enum HomeAction: Equatable {
public struct HomeEnvironment {
public init(
messenger: Messenger,
db: DBManagerGetDB,
dbManager: DBManager,
mainQueue: AnySchedulerOf<DispatchQueue>,
bgQueue: AnySchedulerOf<DispatchQueue>,
register: @escaping () -> RegisterEnvironment,
userSearch: @escaping () -> UserSearchEnvironment
) {
self.messenger = messenger
self.db = db
self.dbManager = dbManager
self.mainQueue = mainQueue
self.bgQueue = bgQueue
self.register = register
......@@ -86,7 +86,7 @@ public struct HomeEnvironment {
}
public var messenger: Messenger
public var db: DBManagerGetDB
public var dbManager: DBManager
public var mainQueue: AnySchedulerOf<DispatchQueue>
public var bgQueue: AnySchedulerOf<DispatchQueue>
public var register: () -> RegisterEnvironment
......@@ -96,7 +96,7 @@ public struct HomeEnvironment {
extension HomeEnvironment {
public static let unimplemented = HomeEnvironment(
messenger: .unimplemented,
db: .unimplemented,
dbManager: .unimplemented,
mainQueue: .unimplemented,
bgQueue: .unimplemented,
register: { .unimplemented },
......@@ -197,13 +197,13 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment>
return .result {
do {
let contactId = try env.messenger.e2e.tryGet().getContact().getId()
let contact = try env.db().fetchContacts(.init(id: [contactId])).first
let contact = try env.dbManager.getDB().fetchContacts(.init(id: [contactId])).first
if let username = contact?.username {
let ud = try env.messenger.ud.tryGet()
try ud.permanentDeleteAccount(username: Fact(fact: username, type: 0))
}
try env.messenger.destroy()
try env.db().drop()
try env.dbManager.removeDB()
return .success(.deleteAccount(.success))
} catch {
return .success(.deleteAccount(.failure(error as NSError)))
......
......@@ -2,6 +2,7 @@ import AppCore
import ComposableArchitecture
import Foundation
import XCTestDynamicOverlay
import XXClient
import XXMessengerClient
import XXModels
......@@ -81,7 +82,8 @@ public let registerReducer = Reducer<RegisterState, RegisterAction, RegisterEnvi
do {
let db = try env.db()
try env.messenger.register(username: username)
let contact = env.messenger.e2e()!.getContact()
var contact = try env.messenger.e2e.tryGet().getContact()
try contact.setFact(.username, username)
try db.saveContact(Contact(
id: try contact.getId(),
marshaled: contact.data,
......
import AppCore
import ComposableArchitecture
import Foundation
import XCTestDynamicOverlay
import XXClient
import XXMessengerClient
import XXModels
public struct SendRequestState: Equatable {
public init(
contact: XXClient.Contact,
myContact: XXClient.Contact? = nil,
sendUsername: Bool = true,
sendEmail: Bool = true,
sendPhone: Bool = true,
isSending: Bool = false,
failure: String? = nil
) {
self.contact = contact
self.myContact = myContact
self.sendUsername = sendUsername
self.sendEmail = sendEmail
self.sendPhone = sendPhone
self.isSending = isSending
self.failure = failure
}
public var contact: XXClient.Contact
public var myContact: XXClient.Contact?
@BindableState public var sendUsername: Bool
@BindableState public var sendEmail: Bool
@BindableState public var sendPhone: Bool
public var isSending: Bool
public var failure: String?
}
public enum SendRequestAction: Equatable, BindableAction {
case start
case sendTapped
case sendSucceeded
case sendFailed(String)
case binding(BindingAction<SendRequestState>)
case myContactFetched(XXClient.Contact?)
}
public struct SendRequestEnvironment {
public init(
messenger: Messenger,
db: DBManagerGetDB,
mainQueue: AnySchedulerOf<DispatchQueue>,
bgQueue: AnySchedulerOf<DispatchQueue>
) {
self.messenger = messenger
self.db = db
self.mainQueue = mainQueue
self.bgQueue = bgQueue
}
public var messenger: Messenger
public var db: DBManagerGetDB
public var mainQueue: AnySchedulerOf<DispatchQueue>
public var bgQueue: AnySchedulerOf<DispatchQueue>
}
#if DEBUG
extension SendRequestEnvironment {
public static let unimplemented = SendRequestEnvironment(
messenger: .unimplemented,
db: .unimplemented,
mainQueue: .unimplemented,
bgQueue: .unimplemented
)
}
#endif
public let sendRequestReducer = Reducer<SendRequestState, SendRequestAction, SendRequestEnvironment>
{ state, action, env in
enum DBFetchEffectID {}
switch action {
case .start:
return Effect
.catching { try env.messenger.e2e.tryGet().getContact().getId() }
.tryMap { try env.db().fetchContactsPublisher(.init(id: [$0])) }
.flatMap { $0 }
.assertNoFailure()
.map(\.first)
.map { $0?.marshaled.map { XXClient.Contact.live($0) } }
.map(SendRequestAction.myContactFetched)
.subscribe(on: env.bgQueue)
.receive(on: env.mainQueue)
.eraseToEffect()
.cancellable(id: DBFetchEffectID.self, cancelInFlight: true)
case .myContactFetched(let contact):
state.myContact = contact
return .none
case .sendTapped:
state.isSending = true
state.failure = nil
return .result { [state] in
func updateAuthStatus(_ authStatus: XXModels.Contact.AuthStatus) throws {
try env.db().bulkUpdateContacts(
.init(id: [try state.contact.getId()]),
.init(authStatus: authStatus)
)
}
do {
try updateAuthStatus(.requesting)
let myFacts = try state.myContact?.getFacts() ?? []
var includedFacts: [Fact] = []
if state.sendUsername, let fact = myFacts.get(.username) {
includedFacts.append(fact)
}
if state.sendEmail, let fact = myFacts.get(.email) {
includedFacts.append(fact)
}
if state.sendPhone, let fact = myFacts.get(.phone) {
includedFacts.append(fact)
}
_ = try env.messenger.e2e.tryGet().requestAuthenticatedChannel(
partner: state.contact,
myFacts: includedFacts
)
try updateAuthStatus(.requested)
return .success(.sendSucceeded)
} catch {
try? updateAuthStatus(.requestFailed)
return .success(.sendFailed(error.localizedDescription))
}
}
.subscribe(on: env.bgQueue)
.receive(on: env.mainQueue)
.eraseToEffect()
case .sendSucceeded:
state.isSending = false
state.failure = nil
return .none
case .sendFailed(let failure):
state.isSending = false
state.failure = failure
return .none
case .binding(_):
return .none
}
}
.binding()
import AppCore
import ComposableArchitecture
import SwiftUI
import XXClient
public struct SendRequestView: View {
public init(store: Store<SendRequestState, SendRequestAction>) {
self.store = store
}
let store: Store<SendRequestState, SendRequestAction>
struct ViewState: Equatable {
var contactUsername: String?
var contactEmail: String?
var contactPhone: String?
var myUsername: String?
var myEmail: String?
var myPhone: String?
var sendUsername: Bool
var sendEmail: Bool
var sendPhone: Bool
var isSending: Bool
var failure: String?
init(state: SendRequestState) {
contactUsername = try? state.contact.getFact(.username)?.fact
contactEmail = try? state.contact.getFact(.email)?.fact
contactPhone = try? state.contact.getFact(.phone)?.fact
myUsername = try? state.myContact?.getFact(.username)?.fact
myEmail = try? state.myContact?.getFact(.email)?.fact
myPhone = try? state.myContact?.getFact(.phone)?.fact
sendUsername = state.sendUsername
sendEmail = state.sendEmail
sendPhone = state.sendPhone
isSending = state.isSending
failure = state.failure
}
}
public var body: some View {
WithViewStore(store.scope(state: ViewState.init)) { viewStore in
Form {
Section {
Button {
viewStore.send(.set(\.$sendUsername, !viewStore.sendUsername))
} label: {
HStack {
Label(viewStore.myUsername ?? "", systemImage: "person")
.tint(Color.primary)
Spacer()
Image(systemName: viewStore.sendUsername ? "checkmark.circle.fill" : "circle")
.foregroundColor(.accentColor)
}
}
.animation(.default, value: viewStore.sendUsername)
Button {
viewStore.send(.set(\.$sendEmail, !viewStore.sendEmail))
} label: {
HStack {
Label(viewStore.myEmail ?? "", systemImage: "envelope")
.tint(Color.primary)
Spacer()
Image(systemName: viewStore.sendEmail ? "checkmark.circle.fill" : "circle")
.foregroundColor(.accentColor)
}
}
.animation(.default, value: viewStore.sendEmail)
Button {
viewStore.send(.set(\.$sendPhone, !viewStore.sendPhone))
} label: {
HStack {
Label(viewStore.myPhone ?? "", systemImage: "phone")
.tint(Color.primary)
Spacer()
Image(systemName: viewStore.sendPhone ? "checkmark.circle.fill" : "circle")
.foregroundColor(.accentColor)
}
}
.animation(.default, value: viewStore.sendPhone)
} header: {
Text("My facts")
}
.disabled(viewStore.isSending)
Section {
Label(viewStore.contactUsername ?? "", systemImage: "person")
Label(viewStore.contactEmail ?? "", systemImage: "envelope")
Label(viewStore.contactPhone ?? "", systemImage: "phone")
} header: {
Text("Contact")
}
Section {
Button {
viewStore.send(.sendTapped)
} label: {
HStack {
Text("Send request")
Spacer()
if viewStore.isSending {
ProgressView()
} else {
Image(systemName: "paperplane")
}
}
}
}
.disabled(viewStore.isSending)
if let failure = viewStore.failure {
Section {
Text(failure)
} header: {
Text("Error")
}
}
}
.navigationTitle("Send Request")
.task { viewStore.send(.start) }
}
}
}
#if DEBUG
public struct SendRequestView_Previews: PreviewProvider {
public static var previews: some View {
NavigationView {
SendRequestView(store: Store(
initialState: SendRequestState(
contact: {
var contact = XXClient.Contact.unimplemented("contact-data".data(using: .utf8)!)
contact.getFactsFromContact.run = { _ in
[
Fact(fact: "contact-username", type: 0),
Fact(fact: "contact-email", type: 1),
Fact(fact: "contact-phone", type: 2),
]
}
return contact
}(),
myContact: {
var contact = XXClient.Contact.unimplemented("my-data".data(using: .utf8)!)
contact.getFactsFromContact.run = { _ in
[
Fact(fact: "my-username", type: 0),
Fact(fact: "my-email", type: 1),
Fact(fact: "my-phone", type: 2),
]
}
return contact
}(),
sendUsername: true,
sendEmail: false,
sendPhone: true,
isSending: false,
failure: "Something went wrong"
),
reducer: .empty,
environment: ()
))
}
}
}
#endif
import ComposableArchitecture
import ComposablePresentation
import ContactFeature
import Foundation
import XCTestDynamicOverlay
import XXClient
......@@ -11,70 +13,60 @@ public struct UserSearchState: Equatable {
case phone
}
public struct Result: Equatable, Identifiable {
public init(
id: Data,
contact: Contact,
username: String? = nil,
email: String? = nil,
phone: String? = nil
) {
self.id = id
self.contact = contact
self.username = username
self.email = email
self.phone = phone
}
public var id: Data
public var contact: XXClient.Contact
public var username: String?
public var email: String?
public var phone: String?
}
public init(
focusedField: Field? = nil,
query: MessengerSearchUsers.Query = .init(),
isSearching: Bool = false,
failure: String? = nil,
results: IdentifiedArrayOf<Result> = []
results: IdentifiedArrayOf<UserSearchResultState> = [],
contact: ContactState? = nil
) {
self.focusedField = focusedField
self.query = query
self.isSearching = isSearching
self.failure = failure
self.results = results
self.contact = contact
}
@BindableState public var focusedField: Field?
@BindableState public var query: MessengerSearchUsers.Query
public var isSearching: Bool
public var failure: String?
public var results: IdentifiedArrayOf<Result>
public var results: IdentifiedArrayOf<UserSearchResultState>
public var contact: ContactState?
}
public enum UserSearchAction: Equatable, BindableAction {
case searchTapped
case didFail(String)
case didSucceed([Contact])
case didDismissContact
case binding(BindingAction<UserSearchState>)
case result(id: UserSearchResultState.ID, action: UserSearchResultAction)
case contact(ContactAction)
}
public struct UserSearchEnvironment {
public init(
messenger: Messenger,
mainQueue: AnySchedulerOf<DispatchQueue>,
bgQueue: AnySchedulerOf<DispatchQueue>
bgQueue: AnySchedulerOf<DispatchQueue>,
result: @escaping () -> UserSearchResultEnvironment,
contact: @escaping () -> ContactEnvironment
) {
self.messenger = messenger
self.mainQueue = mainQueue
self.bgQueue = bgQueue
self.result = result
self.contact = contact
}
public var messenger: Messenger
public var mainQueue: AnySchedulerOf<DispatchQueue>
public var bgQueue: AnySchedulerOf<DispatchQueue>
public var result: () -> UserSearchResultEnvironment
public var contact: () -> ContactEnvironment
}
#if DEBUG
......@@ -82,7 +74,9 @@ extension UserSearchEnvironment {
public static let unimplemented = UserSearchEnvironment(
messenger: .unimplemented,
mainQueue: .unimplemented,
bgQueue: .unimplemented
bgQueue: .unimplemented,
result: { .unimplemented },
contact: { .unimplemented }
)
}
#endif
......@@ -111,14 +105,7 @@ public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSe
state.failure = nil
state.results = IdentifiedArray(uniqueElements: contacts.compactMap { contact in
guard let id = try? contact.getId() else { return nil }
let facts = (try? contact.getFacts()) ?? []
return UserSearchState.Result(
id: id,
contact: contact,
username: facts.first(where: { $0.type == 0 })?.fact,
email: facts.first(where: { $0.type == 1 })?.fact,
phone: facts.first(where: { $0.type == 2 })?.fact
)
return UserSearchResultState(id: id, xxContact: contact)
})
return .none
......@@ -128,8 +115,32 @@ public let userSearchReducer = Reducer<UserSearchState, UserSearchAction, UserSe
state.results = []
return .none
case .binding(_):
case .didDismissContact:
state.contact = nil
return .none
case .result(let id, action: .tapped):
state.contact = ContactState(
id: id,
xxContact: state.results[id: id]?.xxContact
)
return .none
case .binding(_), .result(_, _), .contact(_):
return .none
}
}
.binding()
.presenting(
forEach: userSearchResultReducer,
state: \.results,
action: /UserSearchAction.result(id:action:),
environment: { $0.result() }
)
.presenting(
contactReducer,
state: .keyPath(\.contact),
id: .keyPath(\.?.id),
action: /UserSearchAction.contact,
environment: { $0.contact() }
)
import ComposableArchitecture
import Foundation
import XCTestDynamicOverlay
import XXClient
public struct UserSearchResultState: Equatable, Identifiable {
public init(
id: Data,
xxContact: XXClient.Contact,
username: String? = nil,
email: String? = nil,
phone: String? = nil
) {
self.id = id
self.xxContact = xxContact
self.username = username
self.email = email
self.phone = phone
}
public var id: Data
public var xxContact: XXClient.Contact
public var username: String?
public var email: String?
public var phone: String?
}
public enum UserSearchResultAction: Equatable {
case start
case tapped
}
public struct UserSearchResultEnvironment {
public init() {}
}
#if DEBUG
extension UserSearchResultEnvironment {
public static let unimplemented = UserSearchResultEnvironment()
}
#endif
public let userSearchResultReducer = Reducer<UserSearchResultState, UserSearchResultAction, UserSearchResultEnvironment>
{ state, action, env in
switch action {
case .start:
state.username = try? state.xxContact.getFact(.username)?.fact
state.email = try? state.xxContact.getFact(.email)?.fact
state.phone = try? state.xxContact.getFact(.phone)?.fact
return .none
case .tapped:
return .none
}
}
import ComposableArchitecture
import SwiftUI
import XXModels
public struct UserSearchResultView: View {
public init(store: Store<UserSearchResultState, UserSearchResultAction>) {
self.store = store
}
let store: Store<UserSearchResultState, UserSearchResultAction>
struct ViewState: Equatable {
var username: String?
var email: String?
var phone: String?
init(state: UserSearchResultState) {
username = state.username
email = state.email
phone = state.phone
}
var isEmpty: Bool {
username == nil && email == nil && phone == nil
}
}
public var body: some View {
WithViewStore(store.scope(state: ViewState.init)) { viewStore in
Section {
Button {
viewStore.send(.tapped)
} label: {
HStack {
VStack {
if viewStore.isEmpty {
Image(systemName: "questionmark")
.frame(maxWidth: .infinity)
} else {
if let username = viewStore.username {
Text(username)
}
if let email = viewStore.email {
Text(email)
}
if let phone = viewStore.phone {
Text(phone)
}
}
}
Spacer()
Image(systemName: "chevron.forward")
}
}
}
.task { viewStore.send(.start) }
}
}
}
#if DEBUG
public struct UserSearchResultView_Previews: PreviewProvider {
public static var previews: some View {
UserSearchResultView(store: Store(
initialState: UserSearchResultState(
id: "contact-id".data(using: .utf8)!,
xxContact: .unimplemented("contact-data".data(using: .utf8)!)
),
reducer: .empty,
environment: ()
))
}
}
#endif
import ComposableArchitecture
import ComposablePresentation
import ContactFeature
import SwiftUI
import XXMessengerClient
......@@ -15,14 +17,12 @@ public struct UserSearchView: View {
var query: MessengerSearchUsers.Query
var isSearching: Bool
var failure: String?
var results: IdentifiedArrayOf<UserSearchState.Result>
init(state: UserSearchState) {
focusedField = state.focusedField
query = state.query
isSearching = state.isSearching
failure = state.failure
results = state.results
}
}
......@@ -87,27 +87,25 @@ public struct UserSearchView: View {
}
}
ForEach(viewStore.results) { result in
Section {
if let username = result.username {
Text(username)
}
if let email = result.email {
Text(email)
}
if let phone = result.phone {
Text(phone)
}
if result.username == nil, result.email == nil, result.phone == nil {
Image(systemName: "questionmark")
.frame(maxWidth: .infinity)
}
}
}
ForEachStore(
store.scope(
state: \.results,
action: UserSearchAction.result(id:action:)
),
content: UserSearchResultView.init(store:)
)
}
.onChange(of: viewStore.focusedField) { focusedField = $0 }
.onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) }
.navigationTitle("User Search")
.background(NavigationLinkWithStore(
store.scope(
state: \.contact,
action: UserSearchAction.contact
),
onDeactivate: { viewStore.send(.didDismissContact) },
destination: ContactView.init(store:)
))
}
}
}
......
import Combine
import ComposableArchitecture
import CustomDump
import SendRequestFeature
import XCTest
import XXClient
import XXModels
@testable import ContactFeature
final class ContactFeatureTests: XCTestCase {
func testStart() {
let store = TestStore(
initialState: ContactState(
id: "contact-id".data(using: .utf8)!
),
reducer: contactReducer,
environment: .unimplemented
)
var dbDidFetchContacts: [XXModels.Contact.Query] = []
let dbContactsPublisher = PassthroughSubject<[XXModels.Contact], Error>()
store.environment.mainQueue = .immediate
store.environment.bgQueue = .immediate
store.environment.db.run = {
var db: Database = .failing
db.fetchContactsPublisher.run = { query in
dbDidFetchContacts.append(query)
return dbContactsPublisher.eraseToAnyPublisher()
}
return db
}
store.send(.start)
XCTAssertNoDifference(dbDidFetchContacts, [
.init(id: ["contact-id".data(using: .utf8)!])
])
let dbContact = XXModels.Contact(id: "contact-id".data(using: .utf8)!)
dbContactsPublisher.send([dbContact])
store.receive(.dbContactFetched(dbContact)) {
$0.dbContact = dbContact
}
dbContactsPublisher.send(completion: .finished)
}
func testImportFacts() {
let dbContact: XXModels.Contact = .init(
id: "contact-id".data(using: .utf8)!
)
var xxContact: XXClient.Contact = .unimplemented("contact-data".data(using: .utf8)!)
xxContact.getFactsFromContact.run = { _ in
[
Fact(fact: "contact-username", type: 0),
Fact(fact: "contact-email", type: 1),
Fact(fact: "contact-phone", type: 2),
]
}
let store = TestStore(
initialState: ContactState(
id: "contact-id".data(using: .utf8)!,
dbContact: dbContact,
xxContact: xxContact
),
reducer: contactReducer,
environment: .unimplemented
)
var dbDidSaveContact: [XXModels.Contact] = []
store.environment.mainQueue = .immediate
store.environment.bgQueue = .immediate
store.environment.db.run = {
var db: Database = .failing
db.saveContact.run = { contact in
dbDidSaveContact.append(contact)
return contact
}
return db
}
store.send(.importFactsTapped)
var expectedSavedContact = dbContact
expectedSavedContact.marshaled = xxContact.data
expectedSavedContact.username = "contact-username"
expectedSavedContact.email = "contact-email"
expectedSavedContact.phone = "contact-phone"
XCTAssertNoDifference(dbDidSaveContact, [expectedSavedContact])
}
func testSendRequestWithDBContact() {
var dbContact = XXModels.Contact(id: "contact-id".data(using: .utf8)!)
dbContact.marshaled = "contact-data".data(using: .utf8)!
let store = TestStore(
initialState: ContactState(
id: dbContact.id,
dbContact: dbContact
),
reducer: contactReducer,
environment: .unimplemented
)
store.send(.sendRequestTapped) {
$0.sendRequest = SendRequestState(contact: .live(dbContact.marshaled!))
}
}
func testSendRequestWithXXContact() {
let xxContact = XXClient.Contact.unimplemented("contact-id".data(using: .utf8)!)
let store = TestStore(
initialState: ContactState(
id: "contact-id".data(using: .utf8)!,
xxContact: xxContact
),
reducer: contactReducer,
environment: .unimplemented
)
store.send(.sendRequestTapped) {
$0.sendRequest = SendRequestState(contact: xxContact)
}
}
func testSendRequestDismissed() {
let store = TestStore(
initialState: ContactState(
id: "contact-id".data(using: .utf8)!,
sendRequest: SendRequestState(
contact: .unimplemented("contact-id".data(using: .utf8)!)
)
),
reducer: contactReducer,
environment: .unimplemented
)
store.send(.sendRequestDismissed) {
$0.sendRequest = nil
}
}
func testSendRequestSucceeded() {
let store = TestStore(
initialState: ContactState(
id: "contact-id".data(using: .utf8)!,
sendRequest: SendRequestState(
contact: .unimplemented("contact-id".data(using: .utf8)!)
)
),
reducer: contactReducer,
environment: .unimplemented
)
store.send(.sendRequest(.sendSucceeded)) {
$0.sendRequest = nil
}
}
}
......@@ -316,7 +316,7 @@ final class HomeFeatureTests: XCTestCase {
var dbDidFetchContacts: [XXModels.Contact.Query] = []
var udDidPermanentDeleteAccount: [Fact] = []
var messengerDidDestroy = 0
var dbDidDrop = 0
var didRemoveDB = 0
store.environment.bgQueue = .immediate
store.environment.mainQueue = .immediate
......@@ -329,7 +329,7 @@ final class HomeFeatureTests: XCTestCase {
}
return e2e
}
store.environment.db.run = {
store.environment.dbManager.getDB.run = {
var db: Database = .failing
db.fetchContacts.run = { query in
dbDidFetchContacts.append(query)
......@@ -341,11 +341,11 @@ final class HomeFeatureTests: XCTestCase {
)
]
}
db.drop.run = {
dbDidDrop += 1
}
return db
}
store.environment.dbManager.removeDB.run = {
didRemoveDB += 1
}
store.environment.messenger.ud.get = {
var ud: UserDiscovery = .unimplemented
ud.permanentDeleteAccount.run = { usernameFact in
......@@ -372,7 +372,7 @@ final class HomeFeatureTests: XCTestCase {
XCTAssertNoDifference(dbDidFetchContacts, [.init(id: ["contact-id".data(using: .utf8)!])])
XCTAssertNoDifference(udDidPermanentDeleteAccount, [Fact(fact: "MyUsername", type: 0)])
XCTAssertNoDifference(messengerDidDestroy, 1)
XCTAssertNoDifference(dbDidDrop, 1)
XCTAssertNoDifference(didRemoveDB, 1)
store.receive(.deleteAccount(.success)) {
$0.isDeletingAccount = false
......
......@@ -16,6 +16,7 @@ final class RegisterFeatureTests: XCTestCase {
let now = Date()
let mainQueue = DispatchQueue.test
let bgQueue = DispatchQueue.test
var didSetFactsOnContact: [[XXClient.Fact]] = []
var dbDidSaveContact: [XXModels.Contact] = []
var messengerDidRegisterUsername: [String] = []
......@@ -30,6 +31,11 @@ final class RegisterFeatureTests: XCTestCase {
e2e.getContact.run = {
var contact = XXClient.Contact.unimplemented("contact-data".data(using: .utf8)!)
contact.getIdFromContact.run = { _ in "contact-id".data(using: .utf8)! }
contact.getFactsFromContact.run = { _ in [] }
contact.setFactsOnContact.run = { data, facts in
didSetFactsOnContact.append(facts)
return data
}
return contact
}
return e2e
......@@ -57,6 +63,7 @@ final class RegisterFeatureTests: XCTestCase {
bgQueue.advance()
XCTAssertNoDifference(messengerDidRegisterUsername, ["NewUser"])
XCTAssertNoDifference(didSetFactsOnContact, [[Fact(fact: "NewUser", type: 0)]])
XCTAssertNoDifference(dbDidSaveContact, [
XXModels.Contact(
id: "contact-id".data(using: .utf8)!,
......