use environment object to access services in registration views

This commit is contained in:
Satindar Dhillon 2022-02-23 21:06:54 -08:00
parent 7f9f04989c
commit 201576ec7a
5 changed files with 123 additions and 210 deletions

View file

@ -170,7 +170,7 @@ public struct RootView: View {
#endif
} else {
WelcomeView(services: viewModel.services)
WelcomeView()
.accessibilityElement()
.accessibilityIdentifier("welcomeView")
}

View file

@ -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<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
}
.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<AnyCancellable>()
let performActionSubject = PassthroughSubject<Action, Never>()
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 {

View file

@ -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<AnyCancellable>()
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<AnyCancellable>()
let performActionSubject = PassthroughSubject<Action, Never>()
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))

View file

@ -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<ASAuthorization, Error>)
enum RegistrationState {
case createProfile(userProfile: UserProfile)
case newAppleSignUp(userProfile: UserProfile)
}
@Published var loginError: LoginError?
@Published var registrationState: RegistrationState?
var subscriptions = Set<AnyCancellable>()
let performActionSubject = PassthroughSubject<Action, Never>()
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<ASAuthorization, Error>, 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
}

View file

@ -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)
}