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

Merge branch 'feature/messenger-my-facts' into 'development'

Messenger example - register, confirm, and unregister user facts

See merge request elixxir/elixxir-dapps-sdk-swift!98
parents 1f6824b2 4f3ebddf
No related branches found
No related tags found
2 merge requests!102Release 1.0.0,!98Messenger example - register, confirm, and unregister user facts
Showing
with 1582 additions and 27 deletions
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1400"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "MyContactFeature"
BuildableName = "MyContactFeature"
BlueprintName = "MyContactFeature"
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 = "MyContactFeatureTests"
BuildableName = "MyContactFeatureTests"
BlueprintName = "MyContactFeatureTests"
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 = "MyContactFeature"
BuildableName = "MyContactFeature"
BlueprintName = "MyContactFeature"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
......@@ -26,6 +26,7 @@ let package = Package(
.library(name: "ContactFeature", targets: ["ContactFeature"]),
.library(name: "ContactsFeature", targets: ["ContactsFeature"]),
.library(name: "HomeFeature", targets: ["HomeFeature"]),
.library(name: "MyContactFeature", targets: ["MyContactFeature"]),
.library(name: "RegisterFeature", targets: ["RegisterFeature"]),
.library(name: "RestoreFeature", targets: ["RestoreFeature"]),
.library(name: "SendRequestFeature", targets: ["SendRequestFeature"]),
......@@ -39,7 +40,7 @@ let package = Package(
),
.package(
url: "https://github.com/pointfreeco/swift-composable-architecture.git",
.upToNextMajor(from: "0.40.1")
.upToNextMajor(from: "0.40.2")
),
.package(
url: "https://git.xx.network/elixxir/client-ios-db.git",
......@@ -51,7 +52,11 @@ let package = Package(
),
.package(
url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git",
.upToNextMajor(from: "0.4.0")
.upToNextMajor(from: "0.4.1")
),
.package(
url: "https://github.com/pointfreeco/swift-custom-dump.git",
.upToNextMajor(from: "0.5.2")
),
],
targets: [
......@@ -69,7 +74,8 @@ let package = Package(
.testTarget(
name: "AppCoreTests",
dependencies: [
.target(name: "AppCore")
.target(name: "AppCore"),
.product(name: "CustomDump", package: "swift-custom-dump"),
],
swiftSettings: swiftSettings
),
......@@ -83,6 +89,7 @@ let package = Package(
.target(name: "ContactFeature"),
.target(name: "ContactsFeature"),
.target(name: "HomeFeature"),
.target(name: "MyContactFeature"),
.target(name: "RegisterFeature"),
.target(name: "RestoreFeature"),
.target(name: "SendRequestFeature"),
......@@ -100,6 +107,7 @@ let package = Package(
name: "AppFeatureTests",
dependencies: [
.target(name: "AppFeature"),
.product(name: "CustomDump", package: "swift-custom-dump"),
],
swiftSettings: swiftSettings
),
......@@ -118,6 +126,7 @@ let package = Package(
name: "ChatFeatureTests",
dependencies: [
.target(name: "ChatFeature"),
.product(name: "CustomDump", package: "swift-custom-dump"),
],
swiftSettings: swiftSettings
),
......@@ -135,6 +144,7 @@ let package = Package(
name: "CheckContactAuthFeatureTests",
dependencies: [
.target(name: "CheckContactAuthFeature"),
.product(name: "CustomDump", package: "swift-custom-dump"),
]
),
.target(
......@@ -151,6 +161,7 @@ let package = Package(
name: "ConfirmRequestFeatureTests",
dependencies: [
.target(name: "ConfirmRequestFeature"),
.product(name: "CustomDump", package: "swift-custom-dump"),
]
),
.target(
......@@ -173,6 +184,7 @@ let package = Package(
name: "ContactFeatureTests",
dependencies: [
.target(name: "ContactFeature"),
.product(name: "CustomDump", package: "swift-custom-dump"),
],
swiftSettings: swiftSettings
),
......@@ -181,6 +193,7 @@ let package = Package(
dependencies: [
.target(name: "AppCore"),
.target(name: "ContactFeature"),
.target(name: "MyContactFeature"),
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "ComposablePresentation", package: "swift-composable-presentation"),
.product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
......@@ -193,6 +206,7 @@ let package = Package(
name: "ContactsFeatureTests",
dependencies: [
.target(name: "ContactsFeature"),
.product(name: "CustomDump", package: "swift-custom-dump"),
],
swiftSettings: swiftSettings
),
......@@ -213,6 +227,26 @@ let package = Package(
name: "HomeFeatureTests",
dependencies: [
.target(name: "HomeFeature"),
.product(name: "CustomDump", package: "swift-custom-dump"),
],
swiftSettings: swiftSettings
),
.target(
name: "MyContactFeature",
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: "MyContactFeatureTests",
dependencies: [
.target(name: "MyContactFeature"),
.product(name: "CustomDump", package: "swift-custom-dump"),
],
swiftSettings: swiftSettings
),
......@@ -231,6 +265,7 @@ let package = Package(
name: "RegisterFeatureTests",
dependencies: [
.target(name: "RegisterFeature"),
.product(name: "CustomDump", package: "swift-custom-dump"),
],
swiftSettings: swiftSettings
),
......@@ -245,6 +280,7 @@ let package = Package(
name: "RestoreFeatureTests",
dependencies: [
.target(name: "RestoreFeature"),
.product(name: "CustomDump", package: "swift-custom-dump"),
],
swiftSettings: swiftSettings
),
......@@ -263,6 +299,7 @@ let package = Package(
name: "SendRequestFeatureTests",
dependencies: [
.target(name: "SendRequestFeature"),
.product(name: "CustomDump", package: "swift-custom-dump"),
],
swiftSettings: swiftSettings
),
......@@ -282,6 +319,7 @@ let package = Package(
name: "UserSearchFeatureTests",
dependencies: [
.target(name: "UserSearchFeature"),
.product(name: "CustomDump", package: "swift-custom-dump"),
],
swiftSettings: swiftSettings
),
......@@ -299,6 +337,7 @@ let package = Package(
name: "VerifyContactFeatureTests",
dependencies: [
.target(name: "VerifyContactFeature"),
.product(name: "CustomDump", package: "swift-custom-dump"),
]
),
.target(
......@@ -313,6 +352,7 @@ let package = Package(
name: "WelcomeFeatureTests",
dependencies: [
.target(name: "WelcomeFeature"),
.product(name: "CustomDump", package: "swift-custom-dump"),
],
swiftSettings: swiftSettings
),
......
......@@ -109,6 +109,16 @@
ReferencedContainer = "container:..">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "MyContactFeatureTests"
BuildableName = "MyContactFeatureTests"
BlueprintName = "MyContactFeatureTests"
ReferencedContainer = "container:..">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
......
......@@ -6,6 +6,7 @@ import ContactFeature
import ContactsFeature
import Foundation
import HomeFeature
import MyContactFeature
import RegisterFeature
import RestoreFeature
import SendRequestFeature
......@@ -122,7 +123,15 @@ extension AppEnvironment {
db: dbManager.getDB,
mainQueue: mainQueue,
bgQueue: bgQueue,
contact: { contactEnvironment }
contact: { contactEnvironment },
myContact: {
MyContactEnvironment(
messenger: messenger,
db: dbManager.getDB,
mainQueue: mainQueue,
bgQueue: bgQueue
)
}
)
},
userSearch: {
......
......@@ -3,6 +3,7 @@ import ComposableArchitecture
import ComposablePresentation
import ContactFeature
import Foundation
import MyContactFeature
import XCTestDynamicOverlay
import XXClient
import XXMessengerClient
......@@ -12,16 +13,19 @@ public struct ContactsState: Equatable {
public init(
myId: Data? = nil,
contacts: IdentifiedArrayOf<XXModels.Contact> = [],
contact: ContactState? = nil
contact: ContactState? = nil,
myContact: MyContactState? = nil
) {
self.myId = myId
self.contacts = contacts
self.contact = contact
self.myContact = myContact
}
public var myId: Data?
public var contacts: IdentifiedArrayOf<XXModels.Contact>
public var contact: ContactState?
public var myContact: MyContactState?
}
public enum ContactsAction: Equatable {
......@@ -30,6 +34,9 @@ public enum ContactsAction: Equatable {
case contactSelected(XXModels.Contact)
case contactDismissed
case contact(ContactAction)
case myContactSelected
case myContactDismissed
case myContact(MyContactAction)
}
public struct ContactsEnvironment {
......@@ -38,13 +45,15 @@ public struct ContactsEnvironment {
db: DBManagerGetDB,
mainQueue: AnySchedulerOf<DispatchQueue>,
bgQueue: AnySchedulerOf<DispatchQueue>,
contact: @escaping () -> ContactEnvironment
contact: @escaping () -> ContactEnvironment,
myContact: @escaping () -> MyContactEnvironment
) {
self.messenger = messenger
self.db = db
self.mainQueue = mainQueue
self.bgQueue = bgQueue
self.contact = contact
self.myContact = myContact
}
public var messenger: Messenger
......@@ -52,6 +61,7 @@ public struct ContactsEnvironment {
public var mainQueue: AnySchedulerOf<DispatchQueue>
public var bgQueue: AnySchedulerOf<DispatchQueue>
public var contact: () -> ContactEnvironment
public var myContact: () -> MyContactEnvironment
}
#if DEBUG
......@@ -61,7 +71,8 @@ extension ContactsEnvironment {
db: .unimplemented,
mainQueue: .unimplemented,
bgQueue: .unimplemented,
contact: { .unimplemented }
contact: { .unimplemented },
myContact: { .unimplemented }
)
}
#endif
......@@ -96,7 +107,15 @@ public let contactsReducer = Reducer<ContactsState, ContactsAction, ContactsEnvi
state.contact = nil
return .none
case .contact(_):
case .myContactSelected:
state.myContact = MyContactState()
return .none
case .myContactDismissed:
state.myContact = nil
return .none
case .contact(_), .myContact(_):
return .none
}
}
......@@ -107,3 +126,10 @@ public let contactsReducer = Reducer<ContactsState, ContactsAction, ContactsEnvi
action: /ContactsAction.contact,
environment: { $0.contact() }
)
.presenting(
myContactReducer,
state: .keyPath(\.myContact),
id: .notNil(),
action: /ContactsAction.myContact,
environment: { $0.myContact() }
)
......@@ -2,6 +2,7 @@ import AppCore
import ComposableArchitecture
import ComposablePresentation
import ContactFeature
import MyContactFeature
import SwiftUI
import XXModels
......@@ -28,13 +29,21 @@ public struct ContactsView: View {
ForEach(viewStore.contacts) { contact in
if contact.id == viewStore.myId {
Section {
VStack(alignment: .leading, spacing: 8) {
Label(contact.username ?? "", systemImage: "person")
Label(contact.email ?? "", systemImage: "envelope")
Label(contact.phone ?? "", systemImage: "phone")
Button {
viewStore.send(.myContactSelected)
} label: {
HStack {
VStack(alignment: .leading, spacing: 8) {
Label(contact.username ?? "", systemImage: "person")
Label(contact.email ?? "", systemImage: "envelope")
Label(contact.phone ?? "", systemImage: "phone")
}
.font(.callout)
.tint(Color.primary)
Spacer()
Image(systemName: "chevron.forward")
}
}
.font(.callout)
.tint(Color.primary)
} header: {
Text("My contact")
}
......@@ -70,6 +79,14 @@ public struct ContactsView: View {
onDeactivate: { viewStore.send(.contactDismissed) },
destination: ContactView.init(store:)
))
.background(NavigationLinkWithStore(
store.scope(
state: \.myContact,
action: ContactsAction.myContact
),
onDeactivate: { viewStore.send(.myContactDismissed) },
destination: MyContactView.init(store:)
))
}
}
}
......
import ComposableArchitecture
extension AlertState {
public static func error(_ message: String) -> AlertState<MyContactAction> {
AlertState<MyContactAction>(
title: TextState("Error"),
message: TextState(message),
buttons: []
)
}
}
import AppCore
import Combine
import ComposableArchitecture
import Foundation
import XCTestDynamicOverlay
import XXClient
import XXMessengerClient
import XXModels
public struct MyContactState: Equatable {
public enum Field: String, Hashable {
case email
case emailCode
case phone
case phoneCode
}
public init(
contact: XXModels.Contact? = nil,
focusedField: Field? = nil,
email: String = "",
emailConfirmationID: String? = nil,
emailConfirmationCode: String = "",
isRegisteringEmail: Bool = false,
isConfirmingEmail: Bool = false,
isUnregisteringEmail: Bool = false,
phone: String = "",
phoneConfirmationID: String? = nil,
phoneConfirmationCode: String = "",
isRegisteringPhone: Bool = false,
isConfirmingPhone: Bool = false,
isUnregisteringPhone: Bool = false,
isLoadingFacts: Bool = false,
alert: AlertState<MyContactAction>? = nil
) {
self.contact = contact
self.focusedField = focusedField
self.email = email
self.emailConfirmationID = emailConfirmationID
self.emailConfirmationCode = emailConfirmationCode
self.isRegisteringEmail = isRegisteringEmail
self.isConfirmingEmail = isConfirmingEmail
self.isUnregisteringEmail = isUnregisteringEmail
self.phone = phone
self.phoneConfirmationID = phoneConfirmationID
self.phoneConfirmationCode = phoneConfirmationCode
self.isRegisteringPhone = isRegisteringPhone
self.isConfirmingPhone = isConfirmingPhone
self.isUnregisteringPhone = isUnregisteringPhone
self.isLoadingFacts = isLoadingFacts
self.alert = alert
}
public var contact: XXModels.Contact?
@BindableState public var focusedField: Field?
@BindableState public var email: String
@BindableState public var emailConfirmationID: String?
@BindableState public var emailConfirmationCode: String
@BindableState public var isRegisteringEmail: Bool
@BindableState public var isConfirmingEmail: Bool
@BindableState public var isUnregisteringEmail: Bool
@BindableState public var phone: String
@BindableState public var phoneConfirmationID: String?
@BindableState public var phoneConfirmationCode: String
@BindableState public var isRegisteringPhone: Bool
@BindableState public var isConfirmingPhone: Bool
@BindableState public var isUnregisteringPhone: Bool
@BindableState public var isLoadingFacts: Bool
public var alert: AlertState<MyContactAction>?
}
public enum MyContactAction: Equatable, BindableAction {
case start
case contactFetched(XXModels.Contact?)
case registerEmailTapped
case confirmEmailTapped
case unregisterEmailTapped
case registerPhoneTapped
case confirmPhoneTapped
case unregisterPhoneTapped
case loadFactsTapped
case didFail(String)
case alertDismissed
case binding(BindingAction<MyContactState>)
}
public struct MyContactEnvironment {
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(
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:
state.focusedField = nil
state.isRegisteringEmail = true
return Effect.run { [state] subscriber in
do {
let ud = try env.messenger.ud.tryGet()
let fact = Fact(type: .email, value: state.email)
let confirmationID = try ud.sendRegisterFact(fact)
subscriber.send(.set(\.$emailConfirmationID, confirmationID))
} catch {
subscriber.send(.didFail(error.localizedDescription))
}
subscriber.send(.set(\.$isRegisteringEmail, false))
subscriber.send(completion: .finished)
return AnyCancellable {}
}
.subscribe(on: env.bgQueue)
.receive(on: env.mainQueue)
.eraseToEffect()
case .confirmEmailTapped:
guard let confirmationID = state.emailConfirmationID else { return .none }
state.focusedField = nil
state.isConfirmingEmail = true
return Effect.run { [state] subscriber in
do {
let ud = try env.messenger.ud.tryGet()
try ud.confirmFact(confirmationId: confirmationID, code: state.emailConfirmationCode)
let contactId = try env.messenger.e2e.tryGet().getContact().getId()
if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first {
dbContact.email = state.email
try env.db().saveContact(dbContact)
}
subscriber.send(.set(\.$email, ""))
subscriber.send(.set(\.$emailConfirmationID, nil))
subscriber.send(.set(\.$emailConfirmationCode, ""))
} catch {
subscriber.send(.didFail(error.localizedDescription))
}
subscriber.send(.set(\.$isConfirmingEmail, false))
subscriber.send(completion: .finished)
return AnyCancellable {}
}
.subscribe(on: env.bgQueue)
.receive(on: env.mainQueue)
.eraseToEffect()
case .unregisterEmailTapped:
guard let email = state.contact?.email else { return .none }
state.isUnregisteringEmail = true
return Effect.run { [state] subscriber in
do {
let ud: UserDiscovery = try env.messenger.ud.tryGet()
let fact = Fact(type: .email, value: email)
try ud.removeFact(fact)
let contactId = try env.messenger.e2e.tryGet().getContact().getId()
if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first {
dbContact.email = nil
try env.db().saveContact(dbContact)
}
} catch {
subscriber.send(.didFail(error.localizedDescription))
}
subscriber.send(.set(\.$isUnregisteringEmail, false))
subscriber.send(completion: .finished)
return AnyCancellable {}
}
.subscribe(on: env.bgQueue)
.receive(on: env.mainQueue)
.eraseToEffect()
case .registerPhoneTapped:
state.focusedField = nil
state.isRegisteringPhone = true
return Effect.run { [state] subscriber in
do {
let ud = try env.messenger.ud.tryGet()
let fact = Fact(type: .phone, value: state.phone)
let confirmationID = try ud.sendRegisterFact(fact)
subscriber.send(.set(\.$phoneConfirmationID, confirmationID))
} catch {
subscriber.send(.didFail(error.localizedDescription))
}
subscriber.send(.set(\.$isRegisteringPhone, false))
subscriber.send(completion: .finished)
return AnyCancellable {}
}
.subscribe(on: env.bgQueue)
.receive(on: env.mainQueue)
.eraseToEffect()
case .confirmPhoneTapped:
guard let confirmationID = state.phoneConfirmationID else { return .none }
state.focusedField = nil
state.isConfirmingPhone = true
return Effect.run { [state] subscriber in
do {
let ud = try env.messenger.ud.tryGet()
try ud.confirmFact(confirmationId: confirmationID, code: state.phoneConfirmationCode)
let contactId = try env.messenger.e2e.tryGet().getContact().getId()
if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first {
dbContact.phone = state.phone
try env.db().saveContact(dbContact)
}
subscriber.send(.set(\.$phone, ""))
subscriber.send(.set(\.$phoneConfirmationID, nil))
subscriber.send(.set(\.$phoneConfirmationCode, ""))
} catch {
subscriber.send(.didFail(error.localizedDescription))
}
subscriber.send(.set(\.$isConfirmingPhone, false))
subscriber.send(completion: .finished)
return AnyCancellable {}
}
.subscribe(on: env.bgQueue)
.receive(on: env.mainQueue)
.eraseToEffect()
case .unregisterPhoneTapped:
guard let phone = state.contact?.phone else { return .none }
state.isUnregisteringPhone = true
return Effect.run { [state] subscriber in
do {
let ud: UserDiscovery = try env.messenger.ud.tryGet()
let fact = Fact(type: .phone, value: phone)
try ud.removeFact(fact)
let contactId = try env.messenger.e2e.tryGet().getContact().getId()
if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first {
dbContact.phone = nil
try env.db().saveContact(dbContact)
}
} catch {
subscriber.send(.didFail(error.localizedDescription))
}
subscriber.send(.set(\.$isUnregisteringPhone, false))
subscriber.send(completion: .finished)
return AnyCancellable {}
}
.subscribe(on: env.bgQueue)
.receive(on: env.mainQueue)
.eraseToEffect()
case .loadFactsTapped:
state.isLoadingFacts = true
return Effect.run { subscriber in
do {
let contactId = try env.messenger.e2e.tryGet().getContact().getId()
if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first {
let facts = try env.messenger.ud.tryGet().getFacts()
dbContact.email = facts.get(.email)?.value
dbContact.phone = facts.get(.phone)?.value
try env.db().saveContact(dbContact)
}
} catch {
subscriber.send(.didFail(error.localizedDescription))
}
subscriber.send(.set(\.$isLoadingFacts, false))
subscriber.send(completion: .finished)
return AnyCancellable {}
}
.subscribe(on: env.bgQueue)
.receive(on: env.mainQueue)
.eraseToEffect()
case .didFail(let failure):
state.alert = .error(failure)
return .none
case .alertDismissed:
state.alert = nil
return .none
case .binding(_):
return .none
}
}
.binding()
import ComposableArchitecture
import SwiftUI
import XXModels
public struct MyContactView: View {
public init(store: Store<MyContactState, MyContactAction>) {
self.store = store
}
let store: Store<MyContactState, MyContactAction>
@FocusState var focusedField: MyContactState.Field?
struct ViewState: Equatable {
init(state: MyContactState) {
contact = state.contact
focusedField = state.focusedField
email = state.email
emailConfirmation = state.emailConfirmationID != nil
emailCode = state.emailConfirmationCode
isRegisteringEmail = state.isRegisteringEmail
isConfirmingEmail = state.isConfirmingEmail
isUnregisteringEmail = state.isUnregisteringEmail
phone = state.phone
phoneConfirmation = state.phoneConfirmationID != nil
phoneCode = state.phoneConfirmationCode
isRegisteringPhone = state.isRegisteringPhone
isConfirmingPhone = state.isConfirmingPhone
isUnregisteringPhone = state.isUnregisteringPhone
isLoadingFacts = state.isLoadingFacts
}
var contact: XXModels.Contact?
var focusedField: MyContactState.Field?
var email: String
var emailConfirmation: Bool
var emailCode: String
var isRegisteringEmail: Bool
var isConfirmingEmail: Bool
var isUnregisteringEmail: Bool
var phone: String
var phoneConfirmation: Bool
var phoneCode: String
var isRegisteringPhone: Bool
var isConfirmingPhone: Bool
var isUnregisteringPhone: Bool
var isLoadingFacts: Bool
}
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: {
HStack {
Text("Unregister")
Spacer()
if viewStore.isUnregisteringEmail {
ProgressView()
}
}
}
.disabled(viewStore.isUnregisteringEmail)
} 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)
.disabled(viewStore.isRegisteringEmail || viewStore.emailConfirmation)
if viewStore.emailConfirmation {
TextField(
text: viewStore.binding(
get: \.emailCode,
send: { MyContactAction.set(\.$emailConfirmationCode, $0) }
),
prompt: Text("Enter confirmation code"),
label: { Text("Confirmation code") }
)
.focused($focusedField, equals: .emailCode)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.disabled(viewStore.isConfirmingEmail)
Button {
viewStore.send(.confirmEmailTapped)
} label: {
HStack {
Text("Confirm")
Spacer()
if viewStore.isConfirmingEmail {
ProgressView()
}
}
}
.disabled(viewStore.isConfirmingEmail)
} else {
Button {
viewStore.send(.registerEmailTapped)
} label: {
HStack {
Text("Register")
Spacer()
if viewStore.isRegisteringEmail {
ProgressView()
}
}
}
.disabled(viewStore.isRegisteringEmail)
}
}
} 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: {
HStack {
Text("Unregister")
Spacer()
if viewStore.isUnregisteringPhone {
ProgressView()
}
}
}
.disabled(viewStore.isUnregisteringPhone)
} 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)
.disabled(viewStore.isRegisteringPhone || viewStore.phoneConfirmation)
if viewStore.phoneConfirmation {
TextField(
text: viewStore.binding(
get: \.phoneCode,
send: { MyContactAction.set(\.$phoneConfirmationCode, $0) }
),
prompt: Text("Enter confirmation code"),
label: { Text("Confirmation code") }
)
.focused($focusedField, equals: .phoneCode)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.disabled(viewStore.isConfirmingPhone)
Button {
viewStore.send(.confirmPhoneTapped)
} label: {
HStack {
Text("Confirm")
Spacer()
if viewStore.isConfirmingPhone {
ProgressView()
}
}
}
.disabled(viewStore.isConfirmingPhone)
} else {
Button {
viewStore.send(.registerPhoneTapped)
} label: {
HStack {
Text("Register")
Spacer()
if viewStore.isRegisteringPhone {
ProgressView()
}
}
}
.disabled(viewStore.isRegisteringPhone)
}
}
} else {
Text("")
}
} header: {
Label("Phone", systemImage: "phone")
}
Section {
Button {
viewStore.send(.loadFactsTapped)
} label: {
HStack {
Text("Reload facts")
Spacer()
if viewStore.isLoadingFacts {
ProgressView()
}
}
}
.disabled(viewStore.isLoadingFacts)
} header: {
Text("Actions")
}
}
.navigationTitle("My Contact")
.task { viewStore.send(.start) }
.onChange(of: viewStore.focusedField) { focusedField = $0 }
.onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) }
.alert(store.scope(state: \.alert), dismiss: .alertDismissed)
}
}
}
#if DEBUG
public struct MyContactView_Previews: PreviewProvider {
public static var previews: some View {
NavigationView {
MyContactView(store: Store(
initialState: MyContactState(),
reducer: .empty,
environment: ()
))
}
}
}
#endif
import ComposableArchitecture
import CustomDump
import HomeFeature
import RestoreFeature
import WelcomeFeature
......
......@@ -2,6 +2,7 @@ import Combine
import ComposableArchitecture
import ContactFeature
import CustomDump
import MyContactFeature
import XCTest
import XXClient
import XXMessengerClient
......@@ -94,4 +95,30 @@ final class ContactsFeatureTests: XCTestCase {
$0.contact = nil
}
}
func testSelectMyContact() {
let store = TestStore(
initialState: ContactsState(),
reducer: contactsReducer,
environment: .unimplemented
)
store.send(.myContactSelected) {
$0.myContact = MyContactState()
}
}
func testDismissMyContact() {
let store = TestStore(
initialState: ContactsState(
myContact: MyContactState()
),
reducer: contactsReducer,
environment: .unimplemented
)
store.send(.myContactDismissed) {
$0.myContact = nil
}
}
}
import AppCore
import ComposableArchitecture
import ContactsFeature
import CustomDump
import RegisterFeature
import UserSearchFeature
import XCTest
......
import ComposableArchitecture
import CustomDump
import XCTest
import XXClient
import XXMessengerClient
......
import Combine
import ComposableArchitecture
import CustomDump
import XCTest
import XXClient
import XXModels
......
import ComposableArchitecture
import ContactFeature
import CustomDump
import XCTest
import XXClient
import XXMessengerClient
......@@ -64,6 +65,8 @@ final class UserSearchFeatureTests: XCTestCase {
$0.failure = nil
}
XCTAssertNoDifference(didSearchWithQuery, [.init(username: "Username")])
store.receive(.didSucceed(contacts)) {
$0.isSearching = false
$0.failure = nil
......
......@@ -59,8 +59,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-composable-architecture.git",
"state" : {
"revision" : "cbe013b42b3c368957f8f882c960b93845e1589d",
"version" : "0.40.1"
"revision" : "9ea8c763061287052a68d5e6723fed45e898b7d9",
"version" : "0.40.2"
}
},
{
......@@ -75,10 +75,10 @@
{
"identity" : "swift-custom-dump",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-custom-dump",
"location" : "https://github.com/pointfreeco/swift-custom-dump.git",
"state" : {
"revision" : "21ec1d717c07cea5a026979cb0471dd95c7087e7",
"version" : "0.5.0"
"revision" : "c9b6b940d95c0a925c63f6858943415714d8a981",
"version" : "0.5.2"
}
},
{
......@@ -95,8 +95,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay.git",
"state" : {
"revision" : "38bc9242e4388b80bd23ddfdf3071428859e3260",
"version" : "0.4.0"
"revision" : "30314f1ece684dd60679d598a9b89107557b67d9",
"version" : "0.4.1"
}
}
],
......
......@@ -14,8 +14,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-custom-dump.git",
"state" : {
"revision" : "21ec1d717c07cea5a026979cb0471dd95c7087e7",
"version" : "0.5.0"
"revision" : "c9b6b940d95c0a925c63f6858943415714d8a981",
"version" : "0.5.2"
}
},
{
......@@ -23,8 +23,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay.git",
"state" : {
"revision" : "38bc9242e4388b80bd23ddfdf3071428859e3260",
"version" : "0.4.0"
"revision" : "30314f1ece684dd60679d598a9b89107557b67d9",
"version" : "0.4.1"
}
}
],
......
......@@ -26,11 +26,11 @@ let package = Package(
dependencies: [
.package(
url: "https://github.com/pointfreeco/swift-custom-dump.git",
.upToNextMajor(from: "0.5.0")
.upToNextMajor(from: "0.5.2")
),
.package(
url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git",
.upToNextMajor(from: "0.4.0")
.upToNextMajor(from: "0.4.1")
),
.package(
url: "https://github.com/kishikawakatsumi/KeychainAccess.git",
......
pkill -int com.apple.CoreSimulator.CoreSimulatorService
killall Xcode
rm -rf "$(getconf DARWIN_USER_CACHE_DIR)/org.llvm.clang/ModuleCache"
rm -rf "$(getconf DARWIN_USER_CACHE_DIR)/org.llvm.clang.$(whoami)/ModuleCache"
rm -rf ~/Library/Developer/Xcode/DerivedData/*
rm -rf ~/Library/Caches/com.apple.dt.Xcode/*
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