Skip to content
Snippets Groups Projects
Commit 97727f6f authored by Dariusz Rybicki's avatar Dariusz Rybicki
Browse files

Fetch my contact

parent 6d0f1a18
No related branches found
No related tags found
2 merge requests!102Release 1.0.0,!98Messenger example - register, confirm, and unregister user facts
......@@ -125,7 +125,12 @@ extension AppEnvironment {
bgQueue: bgQueue,
contact: { contactEnvironment },
myContact: {
MyContactEnvironment()
MyContactEnvironment(
messenger: messenger,
db: dbManager.getDB,
mainQueue: mainQueue,
bgQueue: bgQueue
)
}
)
},
......
import AppCore
import ComposableArchitecture
import Foundation
import XCTestDynamicOverlay
import XXClient
import XXMessengerClient
import XXModels
public struct MyContactState: Equatable {
public init() {}
public enum Field: String, Hashable {
case email
case phone
}
public enum MyContactAction: Equatable {
public init(
contact: XXModels.Contact? = nil,
focusedField: Field? = nil,
email: String = "",
phone: String = ""
) {
self.contact = contact
self.focusedField = focusedField
self.email = email
self.phone = phone
}
public var contact: XXModels.Contact?
@BindableState public var focusedField: Field?
@BindableState public var email: String
@BindableState public var phone: String
}
public enum MyContactAction: Equatable, BindableAction {
case start
case contactFetched(XXModels.Contact?)
case registerEmailTapped
case unregisterEmailTapped
case registerPhoneTapped
case unregisterPhoneTapped
case loadFactsTapped
case binding(BindingAction<MyContactState>)
}
public struct MyContactEnvironment {
public init() {}
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 MyContactEnvironment {
public static let unimplemented = MyContactEnvironment()
public static let unimplemented = MyContactEnvironment(
messenger: .unimplemented,
db: .unimplemented,
mainQueue: .unimplemented,
bgQueue: .unimplemented
)
}
#endif
public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContactEnvironment>
{ 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(MyContactAction.contactFetched)
.subscribe(on: env.bgQueue)
.receive(on: env.mainQueue)
.eraseToEffect()
.cancellable(id: DBFetchEffectID.self, cancelInFlight: true)
case .contactFetched(let contact):
state.contact = contact
return .none
case .registerEmailTapped:
return .none
case .unregisterEmailTapped:
return .none
case .registerPhoneTapped:
return .none
case .unregisterPhoneTapped:
return .none
case .loadFactsTapped:
return .none
case .binding(_):
return .none
}
}
.binding()
import ComposableArchitecture
import SwiftUI
import XXModels
public struct MyContactView: View {
public init(store: Store<MyContactState, MyContactAction>) {
......@@ -7,17 +8,113 @@ public struct MyContactView: View {
}
let store: Store<MyContactState, MyContactAction>
@FocusState var focusedField: MyContactState.Field?
struct ViewState: Equatable {
init(state: MyContactState) {}
init(state: MyContactState) {
contact = state.contact
focusedField = state.focusedField
email = state.email
phone = state.phone
}
var contact: XXModels.Contact?
var focusedField: MyContactState.Field?
var email: String
var phone: String
}
public var body: some View {
WithViewStore(store, observe: ViewState.init) { viewStore in
Form {
Section {
Text(viewStore.contact?.username ?? "")
} header: {
Label("Username", systemImage: "person")
}
Section {
if let contact = viewStore.contact {
if let email = contact.email {
Text(email)
Button(role: .destructive) {
viewStore.send(.unregisterEmailTapped)
} label: {
Text("Unregister")
}
} else {
TextField(
text: viewStore.binding(
get: \.email,
send: { MyContactAction.set(\.$email, $0) }
),
prompt: Text("Enter email"),
label: { Text("Email") }
)
.focused($focusedField, equals: .email)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
Button {
viewStore.send(.registerEmailTapped)
} label: {
Text("Register")
}
}
} else {
Text("")
}
} header: {
Label("Email", systemImage: "envelope")
}
Section {
if let contact = viewStore.contact {
if let phone = contact.phone {
Text(phone)
Button(role: .destructive) {
viewStore.send(.unregisterPhoneTapped)
} label: {
Text("Unregister")
}
} else {
TextField(
text: viewStore.binding(
get: \.phone,
send: { MyContactAction.set(\.$phone, $0) }
),
prompt: Text("Enter phone"),
label: { Text("Phone") }
)
.focused($focusedField, equals: .phone)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
Button {
viewStore.send(.registerPhoneTapped)
} label: {
Text("Register")
}
}
} else {
Text("")
}
} header: {
Label("Phone", systemImage: "phone")
}
Section {
Button {
viewStore.send(.loadFactsTapped)
} label: {
Text("Load facts from client")
}
} header: {
Text("Actions")
}
}
.navigationTitle("My Contact")
.task { viewStore.send(.start) }
.onChange(of: viewStore.focusedField) { focusedField = $0 }
.onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) }
}
}
}
......@@ -25,6 +122,7 @@ public struct MyContactView: View {
#if DEBUG
public struct MyContactView_Previews: PreviewProvider {
public static var previews: some View {
NavigationView {
MyContactView(store: Store(
initialState: MyContactState(),
reducer: .empty,
......@@ -32,4 +130,5 @@ public struct MyContactView_Previews: PreviewProvider {
))
}
}
}
#endif
import Combine
import ComposableArchitecture
import CustomDump
import XCTest
import XXClient
import XXMessengerClient
import XXModels
@testable import MyContactFeature
final class MyContactFeatureTests: XCTestCase {
func testStart() {
let contactId = "contact-id".data(using: .utf8)!
let store = TestStore(
initialState: MyContactState(),
reducer: myContactReducer,
environment: .unimplemented
)
var dbDidFetchContacts: [XXModels.Contact.Query] = []
let dbContactsPublisher = PassthroughSubject<[XXModels.Contact], Error>()
store.environment.mainQueue = .immediate
store.environment.bgQueue = .immediate
store.environment.messenger.e2e.get = {
var e2e: E2E = .unimplemented
e2e.getContact.run = {
var contact: XXClient.Contact = .unimplemented(Data())
contact.getIdFromContact.run = { _ in contactId }
return contact
}
return e2e
}
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: [contactId])])
dbContactsPublisher.send([])
store.receive(.contactFetched(nil))
let contact = XXModels.Contact(id: contactId)
dbContactsPublisher.send([contact])
store.receive(.contactFetched(contact)) {
$0.contact = contact
}
dbContactsPublisher.send(completion: .finished)
}
func testRegisterEmail() {
let email = "test@email.com"
let store = TestStore(
initialState: MyContactState(),
reducer: myContactReducer,
environment: .unimplemented
)
store.send(.set(\.$email, email)) {
$0.email = email
}
store.send(.registerEmailTapped)
}
func testUnregisterEmail() {
let store = TestStore(
initialState: MyContactState(),
reducer: myContactReducer,
environment: .unimplemented
)
store.send(.unregisterEmailTapped)
}
func testRegisterPhone() {
let phone = "123456789"
let store = TestStore(
initialState: MyContactState(),
reducer: myContactReducer,
environment: .unimplemented
)
store.send(.set(\.$phone, phone)) {
$0.phone = phone
}
store.send(.registerPhoneTapped)
}
func testUnregisterPhone() {
let store = TestStore(
initialState: MyContactState(),
reducer: myContactReducer,
environment: .unimplemented
)
store.send(.unregisterPhoneTapped)
}
func testLoadFactsFromClient() {
let store = TestStore(
initialState: MyContactState(),
reducer: myContactReducer,
environment: .unimplemented
)
store.send(.loadFactsTapped)
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment