From 52e6c7688baff557e47545f4cae20cb24cd2a18d Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Mon, 28 Feb 2022 18:11:19 -0800 Subject: [PATCH] move CreateProfileView into App package --- .../Views/CreateProfileContainerView.swift | 128 ---------- .../Registration/CreateProfileView.swift | 229 ++++++++++++++++++ .../NewAppleSignupView.swift | 0 .../{ => Registration}/RegistrationView.swift | 2 +- .../Authentication/Authenticator.swift | 2 + .../RegistrationViews/CreateProfileView.swift | 151 ------------ 6 files changed, 232 insertions(+), 280 deletions(-) delete mode 100644 apple/OmnivoreKit/Sources/App/Views/CreateProfileContainerView.swift create mode 100644 apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift rename apple/OmnivoreKit/Sources/App/Views/{ => Registration}/NewAppleSignupView.swift (100%) rename apple/OmnivoreKit/Sources/App/Views/{ => Registration}/RegistrationView.swift (98%) delete mode 100644 apple/OmnivoreKit/Sources/Views/RegistrationViews/CreateProfileView.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/CreateProfileContainerView.swift b/apple/OmnivoreKit/Sources/App/Views/CreateProfileContainerView.swift deleted file mode 100644 index dc9254c64..000000000 --- a/apple/OmnivoreKit/Sources/App/Views/CreateProfileContainerView.swift +++ /dev/null @@ -1,128 +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() - - 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 - @StateObject private var viewModel: CreateProfileViewModel - - @State private var name: String - @State private var bio = "" - - init(userProfile: UserProfile, dataService: DataService) { - self._viewModel = StateObject( - wrappedValue: 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) } - ) - } -} diff --git a/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift new file mode 100644 index 000000000..1261020d6 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/CreateProfileView.swift @@ -0,0 +1,229 @@ +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() + + 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) + } +} + +public struct CreateProfileView: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @EnvironmentObject var authenticator: Authenticator + @EnvironmentObject var dataService: DataService + @StateObject private var viewModel: CreateProfileViewModel + + @State private var name: String + @State private var bio = "" + + init(userProfile: UserProfile, dataService: DataService) { + self._viewModel = StateObject( + wrappedValue: CreateProfileViewModel(initialUserProfile: userProfile, dataService: dataService) + ) + self._name = State(initialValue: userProfile.name) + } + + 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(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: { viewModel.submitProfile(name: name, bio: bio, authenticator: authenticator) }, + 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 + } + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/NewAppleSignupView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/NewAppleSignupView.swift similarity index 100% rename from apple/OmnivoreKit/Sources/App/Views/NewAppleSignupView.swift rename to apple/OmnivoreKit/Sources/App/Views/Registration/NewAppleSignupView.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/RegistrationView.swift b/apple/OmnivoreKit/Sources/App/Views/Registration/RegistrationView.swift similarity index 98% rename from apple/OmnivoreKit/Sources/App/Views/RegistrationView.swift rename to apple/OmnivoreKit/Sources/App/Views/Registration/RegistrationView.swift index b36090bec..4f39d0e3b 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RegistrationView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Registration/RegistrationView.swift @@ -129,7 +129,7 @@ struct RegistrationView: View { var body: some View { if let registrationState = viewModel.registrationState { if case let RegistrationViewModel.RegistrationState.createProfile(userProfile) = registrationState { - CreateProfileContainerView(userProfile: userProfile, dataService: dataService) + CreateProfileView(userProfile: userProfile, dataService: dataService) } else if case let RegistrationViewModel.RegistrationState.newAppleSignUp(userProfile) = registrationState { NewAppleSignupView( userProfile: userProfile, diff --git a/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift b/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift index d973530bb..a31c56ba3 100644 --- a/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift +++ b/apple/OmnivoreKit/Sources/Services/Authentication/Authenticator.swift @@ -1,6 +1,7 @@ import AppAuth import Combine import Foundation +import Models import Utils import WebKit @@ -14,6 +15,7 @@ public final class Authenticator: ObservableObject { } @Published public internal(set) var isLoggedIn: Bool + @Published public var pendinguserProfile = UserProfile(username: "", name: "", bio: nil) let networker: Networker diff --git a/apple/OmnivoreKit/Sources/Views/RegistrationViews/CreateProfileView.swift b/apple/OmnivoreKit/Sources/Views/RegistrationViews/CreateProfileView.swift deleted file mode 100644 index d573e47c9..000000000 --- a/apple/OmnivoreKit/Sources/Views/RegistrationViews/CreateProfileView.swift +++ /dev/null @@ -1,151 +0,0 @@ -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, - potentialUsername: Binding, - 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 - } - } -}