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

Implement MyContactFeature

parent e42c1a97
No related branches found
No related tags found
1 merge request!14[Example App] Make identity & contact
......@@ -145,10 +145,19 @@ let package = Package(
.target(
name: "MyContactFeature",
dependencies: [
.target(name: "ErrorFeature"),
.product(
name: "ComposableArchitecture",
package: "swift-composable-architecture"
),
.product(
name: "ComposablePresentation",
package: "swift-composable-presentation"
),
.product(
name: "ElixxirDAppsSDK",
package: "elixxir-dapps-sdk-swift"
),
],
swiftSettings: swiftSettings
),
......
......@@ -25,6 +25,7 @@ extension AppEnvironment {
static func live() -> AppEnvironment {
let clientSubject = CurrentValueSubject<Client?, Never>(nil)
let identitySubject = CurrentValueSubject<Identity?, Never>(nil)
let contactSubject = CurrentValueSubject<Data?, Never>(nil)
let mainScheduler = DispatchQueue.main.eraseToAnyScheduler()
let bgScheduler = DispatchQueue(
label: "xx.network.dApps.ExampleApp.bg",
......@@ -58,7 +59,15 @@ extension AppEnvironment {
mainScheduler: mainScheduler,
error: ErrorEnvironment()
),
myContact: MyContactEnvironment()
myContact: MyContactEnvironment(
getClient: { clientSubject.value },
getIdentity: { identitySubject.value },
observeContact: { contactSubject.eraseToAnyPublisher() },
updateContact: { contactSubject.value = $0 },
bgScheduler: bgScheduler,
mainScheduler: mainScheduler,
error: ErrorEnvironment()
)
)
)
}
......
import Combine
import ComposableArchitecture
import ComposablePresentation
import ElixxirDAppsSDK
import ErrorFeature
public struct MyContactState: Equatable {
public init(
id: UUID
id: UUID,
contact: Data? = nil,
isMakingContact: Bool = false,
error: ErrorState? = nil
) {
self.id = id
self.contact = contact
self.isMakingContact = isMakingContact
self.error = error
}
public var id: UUID
public var contact: Data?
public var isMakingContact: Bool
public var error: ErrorState?
}
public enum MyContactAction: Equatable {
case viewDidLoad
case observeMyContact
case didUpdateMyContact(Data?)
case makeContact
case didFinishMakingContact(NSError?)
case didDismissError
case error(ErrorAction)
}
public struct MyContactEnvironment {
public init() {}
public init(
getClient: @escaping () -> Client?,
getIdentity: @escaping () -> Identity?,
observeContact: @escaping () -> AnyPublisher<Data?, Never>,
updateContact: @escaping (Data?) -> Void,
bgScheduler: AnySchedulerOf<DispatchQueue>,
mainScheduler: AnySchedulerOf<DispatchQueue>,
error: ErrorEnvironment
) {
self.getClient = getClient
self.getIdentity = getIdentity
self.observeContact = observeContact
self.updateContact = updateContact
self.bgScheduler = bgScheduler
self.mainScheduler = mainScheduler
self.error = error
}
public var getClient: () -> Client?
public var getIdentity: () -> Identity?
public var observeContact: () -> AnyPublisher<Data?, Never>
public var updateContact: (Data?) -> Void
public var bgScheduler: AnySchedulerOf<DispatchQueue>
public var mainScheduler: AnySchedulerOf<DispatchQueue>
public var error: ErrorEnvironment
}
public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContactEnvironment>
{ state, action, env in
switch action {
case .viewDidLoad:
return .merge([
.init(value: .observeMyContact),
])
case .observeMyContact:
struct EffectId: Hashable {
let id: UUID
}
return env.observeContact()
.removeDuplicates()
.map(MyContactAction.didUpdateMyContact)
.subscribe(on: env.bgScheduler)
.receive(on: env.mainScheduler)
.eraseToEffect()
.cancellable(id: EffectId(id: state.id), cancelInFlight: true)
case .didUpdateMyContact(let contact):
state.contact = contact
return .none
case .makeContact:
state.isMakingContact = true
return Effect.future { fulfill in
guard let identity = env.getIdentity() else {
fulfill(.success(.didFinishMakingContact(NoIdentityError() as NSError)))
return
}
do {
env.updateContact(try env.getClient()?.makeContactFromIdentity(identity: identity))
fulfill(.success(.didFinishMakingContact(nil)))
} catch {
fulfill(.success(.didFinishMakingContact(error as NSError)))
}
}
.subscribe(on: env.bgScheduler)
.receive(on: env.mainScheduler)
.eraseToEffect()
case .didFinishMakingContact(let error):
state.isMakingContact = false
if let error = error {
state.error = ErrorState(error: error)
}
return .none
case .didDismissError:
state.error = nil
return .none
case .error(_):
return .none
}
}
public struct NoIdentityError: Error, LocalizedError {
public init() {}
}
#if DEBUG
extension MyContactEnvironment {
public static let failing = MyContactEnvironment()
public static let failing = MyContactEnvironment(
getClient: { fatalError() },
getIdentity: { fatalError() },
observeContact: { fatalError() },
updateContact: { _ in fatalError() },
bgScheduler: .failing,
mainScheduler: .failing,
error: .failing
)
}
#endif
import ComposableArchitecture
import ComposablePresentation
import ElixxirDAppsSDK
import ErrorFeature
import SwiftUI
public struct MyContactView: View {
......@@ -9,17 +12,65 @@ public struct MyContactView: View {
let store: Store<MyContactState, MyContactAction>
struct ViewState: Equatable {
init(state: MyContactState) {}
let contact: Data?
let isMakingContact: Bool
init(state: MyContactState) {
contact = state.contact
isMakingContact = state.isMakingContact
}
var isLoading: Bool {
isMakingContact
}
}
public var body: some View {
WithViewStore(store.scope(state: ViewState.init)) { viewStore in
Text("MyContactView")
.navigationTitle("My contact")
.task {
viewStore.send(.viewDidLoad)
Form {
Section {
Text(string(for: viewStore.contact))
.textSelection(.enabled)
}
Section {
Button {
viewStore.send(.makeContact)
} label: {
HStack {
Text("Make contact from identity")
Spacer()
if viewStore.isMakingContact {
ProgressView()
}
}
}
}
.disabled(viewStore.isLoading)
}
.navigationTitle("My contact")
.navigationBarBackButtonHidden(viewStore.isLoading)
.task {
viewStore.send(.viewDidLoad)
}
.sheet(
store.scope(
state: \.error,
action: MyContactAction.error
),
onDismiss: {
viewStore.send(.didDismissError)
},
content: ErrorView.init(store:)
)
}
}
func string(for contact: Data?) -> String {
guard let contact = contact else {
return "No contact"
}
return String(data: contact, encoding: .utf8) ?? "Decoding error"
}
}
......
import Combine
import ComposableArchitecture
import CustomDump
import ElixxirDAppsSDK
import ErrorFeature
import XCTest
@testable import MyContactFeature
final class MyContactFeatureTests: XCTestCase {
func testExample() {
XCTAssert(true)
func testViewDidLoad() {
let myContactSubject = PassthroughSubject<Data?, Never>()
let bgScheduler = DispatchQueue.test
let mainScheduler = DispatchQueue.test
var env = MyContactEnvironment.failing
env.observeContact = { myContactSubject.eraseToAnyPublisher() }
env.bgScheduler = bgScheduler.eraseToAnyScheduler()
env.mainScheduler = mainScheduler.eraseToAnyScheduler()
let store = TestStore(
initialState: MyContactState(id: UUID()),
reducer: myContactReducer,
environment: env
)
store.send(.viewDidLoad)
store.receive(.observeMyContact)
bgScheduler.advance()
let contact = "\(Int.random(in: 100...999))".data(using: .utf8)!
myContactSubject.send(contact)
mainScheduler.advance()
store.receive(.didUpdateMyContact(contact)) {
$0.contact = contact
}
myContactSubject.send(nil)
mainScheduler.advance()
store.receive(.didUpdateMyContact(nil)) {
$0.contact = nil
}
myContactSubject.send(completion: .finished)
mainScheduler.advance()
}
func testMakeContact() {
let identity = Identity.stub()
let newContact = "\(Int.random(in: 100...999))".data(using: .utf8)!
var didMakeContactFromIdentity = [Identity]()
var didUpdateContact = [Data?]()
let bgScheduler = DispatchQueue.test
let mainScheduler = DispatchQueue.test
var env = MyContactEnvironment.failing
env.getClient = {
var client = Client.failing
client.makeContactFromIdentity.get = { identity in
didMakeContactFromIdentity.append(identity)
return newContact
}
return client
}
env.updateContact = { didUpdateContact.append($0) }
env.getIdentity = { identity }
env.bgScheduler = bgScheduler.eraseToAnyScheduler()
env.mainScheduler = mainScheduler.eraseToAnyScheduler()
let store = TestStore(
initialState: MyContactState(id: UUID()),
reducer: myContactReducer,
environment: env
)
store.send(.makeContact) {
$0.isMakingContact = true
}
bgScheduler.advance()
XCTAssertNoDifference(didMakeContactFromIdentity, [identity])
XCTAssertNoDifference(didUpdateContact, [newContact])
mainScheduler.advance()
store.receive(.didFinishMakingContact(nil)) {
$0.isMakingContact = false
}
}
func testMakeContactWithoutIdentity() {
let error = NoIdentityError() as NSError
let bgScheduler = DispatchQueue.test
let mainScheduler = DispatchQueue.test
var env = MyContactEnvironment.failing
env.getIdentity = { nil }
env.bgScheduler = bgScheduler.eraseToAnyScheduler()
env.mainScheduler = mainScheduler.eraseToAnyScheduler()
let store = TestStore(
initialState: MyContactState(id: UUID()),
reducer: myContactReducer,
environment: env
)
store.send(.makeContact) {
$0.isMakingContact = true
}
bgScheduler.advance()
mainScheduler.advance()
store.receive(.didFinishMakingContact(error)) {
$0.isMakingContact = false
$0.error = ErrorState(error: error)
}
store.send(.didDismissError) {
$0.error = nil
}
}
func testMakeContactFailure() {
let error = NSError(domain: "test", code: 1234)
let bgScheduler = DispatchQueue.test
let mainScheduler = DispatchQueue.test
var env = MyContactEnvironment.failing
env.getClient = {
var client = Client.failing
client.makeContactFromIdentity.get = { _ in throw error }
return client
}
env.getIdentity = { .stub() }
env.bgScheduler = bgScheduler.eraseToAnyScheduler()
env.mainScheduler = mainScheduler.eraseToAnyScheduler()
let store = TestStore(
initialState: MyContactState(id: UUID()),
reducer: myContactReducer,
environment: env
)
store.send(.makeContact) {
$0.isMakingContact = true
}
bgScheduler.advance()
mainScheduler.advance()
store.receive(.didFinishMakingContact(error)) {
$0.isMakingContact = false
$0.error = ErrorState(error: error)
}
store.send(.didDismissError) {
$0.error = nil
}
}
}
private extension Identity {
static func stub() -> Identity {
Identity(
id: "\(Int.random(in: 100...999))".data(using: .utf8)!,
rsaPrivatePem: "\(Int.random(in: 100...999))".data(using: .utf8)!,
salt: "\(Int.random(in: 100...999))".data(using: .utf8)!,
dhKeyPrivate: "\(Int.random(in: 100...999))".data(using: .utf8)!
)
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment