move create profile view into views package
This commit is contained in:
@ -20,10 +20,7 @@ public final class RootViewModel: ObservableObject {
|
||||
@Published fileprivate var snackbarMessage: String?
|
||||
@Published fileprivate var showSnackbar = false
|
||||
|
||||
public enum Action {}
|
||||
|
||||
public var subscriptions = Set<AnyCancellable>()
|
||||
public let performActionSubject = PassthroughSubject<Action, Never>()
|
||||
|
||||
public init() {
|
||||
registerFonts()
|
||||
|
||||
@ -0,0 +1,126 @@
|
||||
import Combine
|
||||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
import Utils
|
||||
import Views
|
||||
|
||||
final class CreateProfileViewModel: ObservableObject {
|
||||
let initialUserProfile: UserProfile
|
||||
|
||||
var hasSuggestedProfile: Bool {
|
||||
!(initialUserProfile.name.isEmpty && initialUserProfile.username.isEmpty)
|
||||
}
|
||||
|
||||
var headlineText: String {
|
||||
hasSuggestedProfile ? "Confirm Your Profile" : "Create Your Profile"
|
||||
}
|
||||
|
||||
var submitButtonText: String {
|
||||
hasSuggestedProfile ? "Confirm" : "Submit"
|
||||
}
|
||||
|
||||
@Published var loginError: LoginError?
|
||||
@Published var validationErrorMessage: String?
|
||||
@Published var potentialUsernameStatus = PotentialUsernameStatus.noUsername
|
||||
@Published var potentialUsername: String
|
||||
|
||||
var subscriptions = Set<AnyCancellable>()
|
||||
|
||||
init(initialUserProfile: UserProfile, dataService: DataService) {
|
||||
self.initialUserProfile = initialUserProfile
|
||||
self.potentialUsername = initialUserProfile.username
|
||||
|
||||
$potentialUsername
|
||||
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] username in
|
||||
self?.validateUsername(username: username, dataService: dataService)
|
||||
})
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
|
||||
func submitProfile(name: String, bio: String, authenticator: Authenticator) {
|
||||
let profileOrError = UserProfile.make(
|
||||
username: potentialUsername,
|
||||
name: name,
|
||||
bio: bio.isEmpty ? nil : bio
|
||||
)
|
||||
|
||||
switch profileOrError {
|
||||
case let .left(userProfile):
|
||||
submitProfile(userProfile: userProfile, authenticator: authenticator)
|
||||
case let .right(errorMessage):
|
||||
validationErrorMessage = errorMessage
|
||||
}
|
||||
}
|
||||
|
||||
func validateUsername(username: String, dataService: DataService) {
|
||||
if let status = PotentialUsernameStatus.validationError(username: username.lowercased()) {
|
||||
potentialUsernameStatus = status
|
||||
return
|
||||
}
|
||||
|
||||
dataService.validateUsernamePublisher(username: username).sink(
|
||||
receiveCompletion: { [weak self] completion in
|
||||
guard case let .failure(usernameError) = completion else { return }
|
||||
switch usernameError {
|
||||
case .tooShort:
|
||||
self?.potentialUsernameStatus = .tooShort
|
||||
case .tooLong:
|
||||
self?.potentialUsernameStatus = .tooLong
|
||||
case .invalidPattern:
|
||||
self?.potentialUsernameStatus = .invalidPattern
|
||||
case .nameUnavailable:
|
||||
self?.potentialUsernameStatus = .unavailable
|
||||
case .internalServer, .unknown:
|
||||
self?.loginError = .unknown
|
||||
case .network:
|
||||
self?.loginError = .network
|
||||
}
|
||||
},
|
||||
receiveValue: { [weak self] in
|
||||
self?.potentialUsernameStatus = .available
|
||||
}
|
||||
)
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
|
||||
func submitProfile(userProfile: UserProfile, authenticator: Authenticator) {
|
||||
authenticator
|
||||
.createAccount(userProfile: userProfile).sink(
|
||||
receiveCompletion: { [weak self] completion in
|
||||
guard case let .failure(loginError) = completion else { return }
|
||||
self?.loginError = loginError
|
||||
},
|
||||
receiveValue: { _ in }
|
||||
)
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
}
|
||||
|
||||
struct CreateProfileContainerView: View {
|
||||
@EnvironmentObject var authenticator: Authenticator
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@ObservedObject private var viewModel: CreateProfileViewModel
|
||||
|
||||
@State private var name: String
|
||||
@State private var bio = ""
|
||||
|
||||
init(userProfile: UserProfile, dataService: DataService) {
|
||||
self.viewModel = CreateProfileViewModel(initialUserProfile: userProfile, dataService: dataService)
|
||||
self._name = State(initialValue: userProfile.name)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
CreateProfileView(
|
||||
name: $name,
|
||||
potentialUsername: $viewModel.potentialUsername,
|
||||
headlineText: viewModel.headlineText,
|
||||
submitButtonText: viewModel.submitButtonText,
|
||||
validationErrorMessage: viewModel.validationErrorMessage,
|
||||
loginError: viewModel.loginError,
|
||||
potentialUsernameStatus: viewModel.potentialUsernameStatus,
|
||||
submitProfileAction: { viewModel.submitProfile(name: $0, bio: $1, authenticator: authenticator) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,231 +0,0 @@
|
||||
import Combine
|
||||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
import Utils
|
||||
import Views
|
||||
|
||||
final class CreateProfileViewModel: ObservableObject {
|
||||
let initialUserProfile: UserProfile
|
||||
|
||||
var hasSuggestedProfile: Bool {
|
||||
!(initialUserProfile.name.isEmpty && initialUserProfile.username.isEmpty)
|
||||
}
|
||||
|
||||
var headlineText: String {
|
||||
hasSuggestedProfile ? "Confirm Your Profile" : "Create Your Profile"
|
||||
}
|
||||
|
||||
var submitButtonText: String {
|
||||
hasSuggestedProfile ? "Confirm" : "Submit"
|
||||
}
|
||||
|
||||
@Published var loginError: LoginError?
|
||||
@Published var validationErrorMessage: String?
|
||||
@Published var potentialUsernameStatus = PotentialUsernameStatus.noUsername
|
||||
@Published var potentialUsername: String
|
||||
|
||||
var subscriptions = Set<AnyCancellable>()
|
||||
|
||||
init(initialUserProfile: UserProfile, dataService: DataService) {
|
||||
self.initialUserProfile = initialUserProfile
|
||||
self.potentialUsername = initialUserProfile.username
|
||||
|
||||
$potentialUsername
|
||||
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] username in
|
||||
self?.validateUsername(username: username, dataService: dataService)
|
||||
})
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
|
||||
func submitProfile(name: String, bio: String, authenticator: Authenticator) {
|
||||
let profileOrError = UserProfile.make(
|
||||
username: potentialUsername,
|
||||
name: name,
|
||||
bio: bio.isEmpty ? nil : bio
|
||||
)
|
||||
|
||||
switch profileOrError {
|
||||
case let .left(userProfile):
|
||||
submitProfile(userProfile: userProfile, authenticator: authenticator)
|
||||
case let .right(errorMessage):
|
||||
validationErrorMessage = errorMessage
|
||||
}
|
||||
}
|
||||
|
||||
func validateUsername(username: String, dataService: DataService) {
|
||||
if let status = PotentialUsernameStatus.validationError(username: username.lowercased()) {
|
||||
potentialUsernameStatus = status
|
||||
return
|
||||
}
|
||||
|
||||
dataService.validateUsernamePublisher(username: username).sink(
|
||||
receiveCompletion: { [weak self] completion in
|
||||
guard case let .failure(usernameError) = completion else { return }
|
||||
switch usernameError {
|
||||
case .tooShort:
|
||||
self?.potentialUsernameStatus = .tooShort
|
||||
case .tooLong:
|
||||
self?.potentialUsernameStatus = .tooLong
|
||||
case .invalidPattern:
|
||||
self?.potentialUsernameStatus = .invalidPattern
|
||||
case .nameUnavailable:
|
||||
self?.potentialUsernameStatus = .unavailable
|
||||
case .internalServer, .unknown:
|
||||
self?.loginError = .unknown
|
||||
case .network:
|
||||
self?.loginError = .network
|
||||
}
|
||||
},
|
||||
receiveValue: { [weak self] in
|
||||
self?.potentialUsernameStatus = .available
|
||||
}
|
||||
)
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
|
||||
func submitProfile(userProfile: UserProfile, authenticator: Authenticator) {
|
||||
authenticator
|
||||
.createAccount(userProfile: userProfile).sink(
|
||||
receiveCompletion: { [weak self] completion in
|
||||
guard case let .failure(loginError) = completion else { return }
|
||||
self?.loginError = loginError
|
||||
},
|
||||
receiveValue: { _ in }
|
||||
)
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
}
|
||||
|
||||
struct CreateProfileView: View {
|
||||
@EnvironmentObject var authenticator: Authenticator
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||
@ObservedObject private var viewModel: CreateProfileViewModel
|
||||
|
||||
@State private var name: String
|
||||
@State private var bio = ""
|
||||
|
||||
init(userProfile: UserProfile, dataService: DataService) {
|
||||
self.viewModel = CreateProfileViewModel(initialUserProfile: userProfile, dataService: dataService)
|
||||
self._name = State(initialValue: userProfile.name)
|
||||
}
|
||||
|
||||
private func didTapSubmitButton() {
|
||||
viewModel.submitProfile(name: name, bio: bio, authenticator: authenticator)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
VStack(spacing: 28) {
|
||||
ScrollView(showsIndicators: false) {
|
||||
if horizontalSizeClass == .regular {
|
||||
Spacer(minLength: 150)
|
||||
}
|
||||
VStack(alignment: .center, spacing: 16) {
|
||||
Text(viewModel.headlineText)
|
||||
.font(.appTitle)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.bottom, horizontalSizeClass == .compact ? 0 : 50)
|
||||
.padding(.top, horizontalSizeClass == .compact ? 30 : 0)
|
||||
|
||||
VStack(spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Name")
|
||||
.font(.appFootnote)
|
||||
.foregroundColor(.appGrayText)
|
||||
#if os(iOS)
|
||||
TextField("", text: $name)
|
||||
.textContentType(.name)
|
||||
.keyboardType(.alphabet)
|
||||
#elseif os(macOS)
|
||||
TextField("", text: $name)
|
||||
#endif
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Username")
|
||||
.font(.appFootnote)
|
||||
.foregroundColor(.appGrayText)
|
||||
TextField("", text: $viewModel.potentialUsername)
|
||||
}
|
||||
|
||||
if viewModel.potentialUsernameStatus == .available {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.appBody)
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
}
|
||||
if let message = viewModel.potentialUsernameStatus.message {
|
||||
Text(message)
|
||||
.font(.appCaption)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
.animation(.default)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Bio (optional)")
|
||||
.font(.appFootnote)
|
||||
.foregroundColor(.appGrayText)
|
||||
TextEditor(text: $bio)
|
||||
.lineSpacing(6)
|
||||
.accentColor(.appGraySolid)
|
||||
.foregroundColor(.appGrayText)
|
||||
.font(.appBody)
|
||||
.padding(12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.strokeBorder(Color.appGrayBorder, lineWidth: 1)
|
||||
.background(RoundedRectangle(cornerRadius: 16).fill(Color.systemBackground))
|
||||
)
|
||||
.frame(height: 160)
|
||||
}
|
||||
|
||||
Button(
|
||||
action: didTapSubmitButton,
|
||||
label: { Text(viewModel.submitButtonText) }
|
||||
)
|
||||
.buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300))
|
||||
|
||||
if let errorMessage = viewModel.validationErrorMessage {
|
||||
Text(errorMessage)
|
||||
.font(.appCaption)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
|
||||
if let loginError = viewModel.loginError, viewModel.validationErrorMessage == nil {
|
||||
LoginErrorMessageView(loginError: loginError)
|
||||
}
|
||||
}
|
||||
.textFieldStyle(StandardTextFieldStyle())
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: 300)
|
||||
}
|
||||
}
|
||||
|
||||
private extension PotentialUsernameStatus {
|
||||
var message: String? {
|
||||
switch self {
|
||||
case .tooShort:
|
||||
return "Username must contain at least 4 characters"
|
||||
case .tooLong:
|
||||
return "Username must be less than 15 characters"
|
||||
case .invalidPattern:
|
||||
return "Username can contain only letters and numbers"
|
||||
case .unavailable:
|
||||
return "This name is not available"
|
||||
case .noUsername, .available:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -129,7 +129,7 @@ struct RegistrationView: View {
|
||||
var body: some View {
|
||||
if let registrationState = viewModel.registrationState {
|
||||
if case let RegistrationViewModel.RegistrationState.createProfile(userProfile) = registrationState {
|
||||
CreateProfileView(userProfile: userProfile, dataService: dataService)
|
||||
CreateProfileContainerView(userProfile: userProfile, dataService: dataService)
|
||||
} else if case let RegistrationViewModel.RegistrationState.newAppleSignUp(userProfile) = registrationState {
|
||||
NewAppleSignupView(
|
||||
userProfile: userProfile,
|
||||
|
||||
@ -0,0 +1,151 @@
|
||||
import Models
|
||||
import SwiftUI
|
||||
import Utils
|
||||
|
||||
public struct CreateProfileView: View {
|
||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||
|
||||
@State private var bio = ""
|
||||
|
||||
@Binding var name: String
|
||||
@Binding var potentialUsername: String
|
||||
let headlineText: String
|
||||
let submitButtonText: String
|
||||
let validationErrorMessage: String?
|
||||
let loginError: LoginError?
|
||||
let potentialUsernameStatus: PotentialUsernameStatus
|
||||
let submitProfileAction: (String, String) -> Void
|
||||
|
||||
public init(
|
||||
name: Binding<String>,
|
||||
potentialUsername: Binding<String>,
|
||||
headlineText: String,
|
||||
submitButtonText: String,
|
||||
validationErrorMessage: String?,
|
||||
loginError: LoginError?,
|
||||
potentialUsernameStatus: PotentialUsernameStatus,
|
||||
submitProfileAction: @escaping (String, String) -> Void
|
||||
) {
|
||||
self._name = name
|
||||
self._potentialUsername = potentialUsername
|
||||
self.headlineText = headlineText
|
||||
self.submitButtonText = submitButtonText
|
||||
self.validationErrorMessage = validationErrorMessage
|
||||
self.loginError = loginError
|
||||
self.potentialUsernameStatus = potentialUsernameStatus
|
||||
self.submitProfileAction = submitProfileAction
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
VStack(spacing: 28) {
|
||||
ScrollView(showsIndicators: false) {
|
||||
if horizontalSizeClass == .regular {
|
||||
Spacer(minLength: 150)
|
||||
}
|
||||
VStack(alignment: .center, spacing: 16) {
|
||||
Text(headlineText)
|
||||
.font(.appTitle)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.bottom, horizontalSizeClass == .compact ? 0 : 50)
|
||||
.padding(.top, horizontalSizeClass == .compact ? 30 : 0)
|
||||
|
||||
VStack(spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Name")
|
||||
.font(.appFootnote)
|
||||
.foregroundColor(.appGrayText)
|
||||
#if os(iOS)
|
||||
TextField("", text: $name)
|
||||
.textContentType(.name)
|
||||
.keyboardType(.alphabet)
|
||||
#elseif os(macOS)
|
||||
TextField("", text: $name)
|
||||
#endif
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Username")
|
||||
.font(.appFootnote)
|
||||
.foregroundColor(.appGrayText)
|
||||
TextField("", text: $potentialUsername)
|
||||
}
|
||||
|
||||
if potentialUsernameStatus == .available {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.appBody)
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
}
|
||||
if let message = potentialUsernameStatus.message {
|
||||
Text(message)
|
||||
.font(.appCaption)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
.animation(.default)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text("Bio (optional)")
|
||||
.font(.appFootnote)
|
||||
.foregroundColor(.appGrayText)
|
||||
TextEditor(text: $bio)
|
||||
.lineSpacing(6)
|
||||
.accentColor(.appGraySolid)
|
||||
.foregroundColor(.appGrayText)
|
||||
.font(.appBody)
|
||||
.padding(12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.strokeBorder(Color.appGrayBorder, lineWidth: 1)
|
||||
.background(RoundedRectangle(cornerRadius: 16).fill(Color.systemBackground))
|
||||
)
|
||||
.frame(height: 160)
|
||||
}
|
||||
|
||||
Button(
|
||||
action: { submitProfileAction(name, bio) },
|
||||
label: { Text(submitButtonText) }
|
||||
)
|
||||
.buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300))
|
||||
|
||||
if let errorMessage = validationErrorMessage {
|
||||
Text(errorMessage)
|
||||
.font(.appCaption)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
|
||||
if let loginError = loginError, validationErrorMessage == nil {
|
||||
LoginErrorMessageView(loginError: loginError)
|
||||
}
|
||||
}
|
||||
.textFieldStyle(StandardTextFieldStyle())
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: 300)
|
||||
}
|
||||
}
|
||||
|
||||
private extension PotentialUsernameStatus {
|
||||
var message: String? {
|
||||
switch self {
|
||||
case .tooShort:
|
||||
return "Username must contain at least 4 characters"
|
||||
case .tooLong:
|
||||
return "Username must be less than 15 characters"
|
||||
case .invalidPattern:
|
||||
return "Username can contain only letters and numbers"
|
||||
case .unavailable:
|
||||
return "This name is not available"
|
||||
case .noUsername, .available:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user