From 201576ec7a85b71c2538ef2168c05725404ed6c0 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 23 Feb 2022 21:06:54 -0800 Subject: [PATCH] use environment object to access services in registration views --- .../Sources/Binders/RootViewModel.swift | 2 +- .../Binders/Views/CreateProfileView.swift | 129 +++++++--------- .../Binders/Views/NewAppleSignupView.swift | 60 ++------ .../Binders/Views/RegistrationView.swift | 139 +++++++----------- .../Sources/Binders/Views/WelcomeView.swift | 3 +- 5 files changed, 123 insertions(+), 210 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift b/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift index 6957d492d..662d83dbe 100644 --- a/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift +++ b/apple/OmnivoreKit/Sources/Binders/RootViewModel.swift @@ -170,7 +170,7 @@ public struct RootView: View { #endif } else { - WelcomeView(services: viewModel.services) + WelcomeView() .accessibilityElement() .accessibilityIdentifier("welcomeView") } diff --git a/apple/OmnivoreKit/Sources/Binders/Views/CreateProfileView.swift b/apple/OmnivoreKit/Sources/Binders/Views/CreateProfileView.swift index 45d85e632..06f608c10 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/CreateProfileView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/CreateProfileView.swift @@ -5,26 +5,56 @@ import SwiftUI import Utils import Views -extension CreateProfileViewModel { - static func make(services: Services, pendingUserProfile: UserProfile) -> CreateProfileViewModel { - let viewModel = CreateProfileViewModel(initialUserProfile: pendingUserProfile) - viewModel.bind(services: services) - return viewModel +final class CreateProfileViewModel: ObservableObject { + let initialUserProfile: UserProfile + + var hasSuggestedProfile: Bool { + !(initialUserProfile.name.isEmpty && initialUserProfile.username.isEmpty) } - func bind(services: Services) { - performActionSubject.sink { [weak self] action in - switch action { - case let .submitProfile(userProfile): - self?.submitProfile(userProfile: userProfile, authenticator: services.authenticator) - case let .validateUsername(username: username): - self?.validateUsername(username: username, dataService: services.dataService) - } + 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 } - .store(in: &subscriptions) } - private func validateUsername(username: String, dataService: DataService) { + func validateUsername(username: String, dataService: DataService) { if let status = PotentialUsernameStatus.validationError(username: username.lowercased()) { potentialUsernameStatus = status return @@ -55,7 +85,7 @@ extension CreateProfileViewModel { .store(in: &subscriptions) } - private func submitProfile(userProfile: UserProfile, authenticator: Authenticator) { + func submitProfile(userProfile: UserProfile, authenticator: Authenticator) { authenticator .createAccount(userProfile: userProfile).sink( receiveCompletion: { [weak self] completion in @@ -68,77 +98,22 @@ extension CreateProfileViewModel { } } -// TODO: remove this view model -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 - - enum Action { - case submitProfile(userProfile: UserProfile) - case validateUsername(username: String) - } - - var subscriptions = Set() - let performActionSubject = PassthroughSubject() - - init(initialUserProfile: UserProfile) { - self.initialUserProfile = initialUserProfile - self.potentialUsername = initialUserProfile.username - - $potentialUsername - .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main) - .sink(receiveValue: { [weak self] username in - self?.performActionSubject.send(.validateUsername(username: username)) - }) - .store(in: &subscriptions) - } - - func submitProfile(name: String, bio: String) { - let profileOrError = UserProfile.make( - username: potentialUsername, - name: name, - bio: bio.isEmpty ? nil : bio - ) - - switch profileOrError { - case let .left(userProfile): - performActionSubject.send(.submitProfile(userProfile: userProfile)) - case let .right(errorMessage): - validationErrorMessage = errorMessage - } - } -} - 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(viewModel: CreateProfileViewModel) { - self.viewModel = viewModel - self._name = State(initialValue: viewModel.initialUserProfile.name) + 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) + viewModel.submitProfile(name: name, bio: bio, authenticator: authenticator) } var body: some View { diff --git a/apple/OmnivoreKit/Sources/Binders/Views/NewAppleSignupView.swift b/apple/OmnivoreKit/Sources/Binders/Views/NewAppleSignupView.swift index 2f1046da4..d433ede10 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/NewAppleSignupView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/NewAppleSignupView.swift @@ -5,30 +5,17 @@ import SwiftUI import Utils import Views -extension NewAppleSignupViewModel { - static func make( - services: Services, - userProfile: UserProfile, - showProfileEditView: @escaping () -> Void - ) -> NewAppleSignupViewModel { - let viewModel = NewAppleSignupViewModel(userProfile: userProfile) - viewModel.bind(services: services, showProfileEditView: showProfileEditView) - return viewModel +final class NewAppleSignupViewModel: ObservableObject { + let userProfile: UserProfile + @Published var loginError: LoginError? + + var subscriptions = Set() + + init(userProfile: UserProfile) { + self.userProfile = userProfile } - func bind(services: Services, showProfileEditView: @escaping () -> Void) { - performActionSubject.sink { [weak self] action in - switch action { - case let .acceptProfile(userProfile: userProfile): - self?.submitProfile(userProfile: userProfile, authenticator: services.authenticator) - case .changeProfile: - showProfileEditView() - } - } - .store(in: &subscriptions) - } - - private func submitProfile(userProfile: UserProfile, authenticator: Authenticator) { + func submitProfile(authenticator: Authenticator) { authenticator .createAccount(userProfile: userProfile).sink( receiveCompletion: { [weak self] completion in @@ -41,29 +28,14 @@ extension NewAppleSignupViewModel { } } -// TODO: remove this view model -final class NewAppleSignupViewModel: ObservableObject { - let userProfile: UserProfile - @Published var loginError: LoginError? - - enum Action { - case acceptProfile(userProfile: UserProfile) - case changeProfile - } - - var subscriptions = Set() - let performActionSubject = PassthroughSubject() - - init(userProfile: UserProfile) { - self.userProfile = userProfile - } -} - struct NewAppleSignupView: View { + @EnvironmentObject var authenticator: Authenticator @ObservedObject private var viewModel: NewAppleSignupViewModel + let showProfileEditView: () -> Void - init(viewModel: NewAppleSignupViewModel) { - self.viewModel = viewModel + init(userProfile: UserProfile, showProfileEditView: @escaping () -> Void) { + self.showProfileEditView = showProfileEditView + self.viewModel = NewAppleSignupViewModel(userProfile: userProfile) } var body: some View { @@ -83,13 +55,13 @@ struct NewAppleSignupView: View { VStack { Button( - action: { viewModel.performActionSubject.send(.acceptProfile(userProfile: viewModel.userProfile)) }, + action: { viewModel.submitProfile(authenticator: authenticator) }, label: { Text("Continue") } ) .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) Button( - action: { viewModel.performActionSubject.send(.changeProfile) }, + action: showProfileEditView, label: { Text("Change Username") } ) .buttonStyle(SolidCapsuleButtonStyle(color: .appDeepBackground, width: 300)) diff --git a/apple/OmnivoreKit/Sources/Binders/Views/RegistrationView.swift b/apple/OmnivoreKit/Sources/Binders/Views/RegistrationView.swift index f33a07db4..aabb45de8 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/RegistrationView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/RegistrationView.swift @@ -6,61 +6,38 @@ import SwiftUI import Utils import Views -// TODO: remove this view model final class RegistrationViewModel: ObservableObject { - @Published var loginError: LoginError? - @Published var createProfileViewModel: CreateProfileViewModel? - @Published var newAppleSignupViewModel: NewAppleSignupViewModel? - - enum Action { - case googleButtonTapped - case appleSignInCompleted(result: Result) + enum RegistrationState { + case createProfile(userProfile: UserProfile) + case newAppleSignUp(userProfile: UserProfile) } + @Published var loginError: LoginError? + @Published var registrationState: RegistrationState? + var subscriptions = Set() - let performActionSubject = PassthroughSubject() - init() {} -} - -extension RegistrationViewModel { - static func make(services: Services) -> RegistrationViewModel { - let viewModel = RegistrationViewModel() - viewModel.bind(services: services) - return viewModel - } - - func bind(services: Services) { - performActionSubject.sink { [weak self] action in - self?.loginError = nil - - switch action { - case .googleButtonTapped: - self?.handleGoogleAuth(services: services) - case let .appleSignInCompleted(result: result): - switch AppleSigninPayload.parse(authResult: result) { - case let .success(payload): - self?.handleAppleToken(payload: payload, services: services) - case let .failure(error): - switch error { - case .unauthorized, .unknown: - break - case .network: - self?.loginError = error - } - } + func handleAppleSignInCompletion(result: Result, authenticator: Authenticator) { + switch AppleSigninPayload.parse(authResult: result) { + case let .success(payload): + handleAppleToken(payload: payload, authenticator: authenticator) + case let .failure(error): + switch error { + case .unauthorized, .unknown: + break + case .network: + loginError = error } } - .store(in: &subscriptions) } - private func handleAppleToken(payload: AppleSigninPayload, services: Services) { - services.authenticator.submitAppleToken(token: payload.token).sink( + private func handleAppleToken(payload: AppleSigninPayload, authenticator: Authenticator) { + authenticator.submitAppleToken(token: payload.token).sink( receiveCompletion: { [weak self] completion in guard case let .failure(loginError) = completion else { return } switch loginError { case .unauthorized, .unknown: - self?.handleAppleSignUp(services: services, payload: payload) + self?.handleAppleSignUp(authenticator: authenticator, payload: payload) case .network: self?.loginError = loginError } @@ -70,8 +47,8 @@ extension RegistrationViewModel { .store(in: &subscriptions) } - private func handleAppleSignUp(services: Services, payload: AppleSigninPayload) { - services.authenticator + private func handleAppleSignUp(authenticator: Authenticator, payload: AppleSigninPayload) { + authenticator .createPendingAccountUsingApple(token: payload.token, name: payload.fullName) .sink( receiveCompletion: { [weak self] completion in @@ -80,23 +57,17 @@ extension RegistrationViewModel { }, receiveValue: { [weak self] userProfile in if userProfile.name.isEmpty { - self?.showProfileEditView(services: services, pendingUserProfile: userProfile) + self?.registrationState = .createProfile(userProfile: userProfile) } else { - self?.newAppleSignupViewModel = NewAppleSignupViewModel.make( - services: services, - userProfile: userProfile, - showProfileEditView: { - self?.showProfileEditView(services: services, pendingUserProfile: userProfile) - } - ) + self?.registrationState = .newAppleSignUp(userProfile: userProfile) } } ) .store(in: &subscriptions) } - private func handleGoogleAuth(services: Services) { - services.authenticator + func handleGoogleAuth(authenticator: Authenticator) { + authenticator .handleGoogleAuth(presentingViewController: presentingViewController()) .sink( receiveCompletion: { [weak self] completion in @@ -105,41 +76,19 @@ extension RegistrationViewModel { }, receiveValue: { [weak self] isNewAccount in if isNewAccount { - let pendingUserProfile = UserProfile(username: "", name: "") - self?.showProfileEditView(services: services, pendingUserProfile: pendingUserProfile) + self?.registrationState = .createProfile(userProfile: UserProfile(username: "", name: "")) } } ) .store(in: &subscriptions) } - - func showProfileEditView(services: Services, pendingUserProfile: UserProfile) { - createProfileViewModel = CreateProfileViewModel.make( - services: services, - pendingUserProfile: pendingUserProfile - ) - newAppleSignupViewModel = nil - } -} - -private func presentingViewController() -> PlatformViewController? { - #if os(iOS) - return UIApplication.shared.windows - .filter(\.isKeyWindow) - .first? - .rootViewController - #elseif os(macOS) - return nil - #endif } struct RegistrationView: View { + @EnvironmentObject var authenticator: Authenticator + @EnvironmentObject var dataService: DataService @Environment(\.horizontalSizeClass) var horizontalSizeClass - @ObservedObject private var viewModel: RegistrationViewModel - - init(viewModel: RegistrationViewModel) { - self.viewModel = viewModel - } + @ObservedObject private var viewModel = RegistrationViewModel() var authenticationView: some View { VStack(spacing: 0) { @@ -156,12 +105,12 @@ struct RegistrationView: View { .padding(.top, horizontalSizeClass == .compact ? 30 : 0) AppleSignInButton { - viewModel.performActionSubject.send(.appleSignInCompleted(result: $0)) + viewModel.handleAppleSignInCompletion(result: $0, authenticator: authenticator) } if AppKeys.sharedInstance?.iosClientGoogleId != nil { GoogleAuthButton { - viewModel.performActionSubject.send(.googleButtonTapped) + viewModel.handleGoogleAuth(authenticator: authenticator) } } } @@ -178,12 +127,30 @@ struct RegistrationView: View { } var body: some View { - if let createProfileViewModel = viewModel.createProfileViewModel { - CreateProfileView(viewModel: createProfileViewModel) - } else if let newAppleSignupViewModel = viewModel.newAppleSignupViewModel { - NewAppleSignupView(viewModel: newAppleSignupViewModel) + if let registrationState = viewModel.registrationState { + if case let RegistrationViewModel.RegistrationState.createProfile(userProfile) = registrationState { + CreateProfileView(userProfile: userProfile, dataService: dataService) + } else if case let RegistrationViewModel.RegistrationState.newAppleSignUp(userProfile) = registrationState { + NewAppleSignupView( + userProfile: userProfile, + showProfileEditView: { viewModel.registrationState = .createProfile(userProfile: userProfile) } + ) + } else { + authenticationView + } } else { authenticationView } } } + +private func presentingViewController() -> PlatformViewController? { + #if os(iOS) + return UIApplication.shared.windows + .filter(\.isKeyWindow) + .first? + .rootViewController + #elseif os(macOS) + return nil + #endif +} diff --git a/apple/OmnivoreKit/Sources/Binders/Views/WelcomeView.swift b/apple/OmnivoreKit/Sources/Binders/Views/WelcomeView.swift index f88e7983e..757893380 100644 --- a/apple/OmnivoreKit/Sources/Binders/Views/WelcomeView.swift +++ b/apple/OmnivoreKit/Sources/Binders/Views/WelcomeView.swift @@ -7,7 +7,6 @@ import Views struct WelcomeView: View { @EnvironmentObject var dataService: DataService @Environment(\.horizontalSizeClass) var horizontalSizeClass - let services: Services @State private var showRegistrationView = false @State private var isKeyboardOnScreen = false @State private var showDebugModal = false @@ -21,7 +20,7 @@ struct WelcomeView: View { @ViewBuilder func userInteractiveView(width: CGFloat) -> some View { Group { if showRegistrationView { - RegistrationView(viewModel: RegistrationViewModel.make(services: services)) + RegistrationView() } else { GetStartedView(showRegistrationView: $showRegistrationView) }