mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
use environment object to access services in registration views
This commit is contained in:
parent
7f9f04989c
commit
201576ec7a
5 changed files with 123 additions and 210 deletions
|
|
@ -170,7 +170,7 @@ public struct RootView: View {
|
|||
#endif
|
||||
|
||||
} else {
|
||||
WelcomeView(services: viewModel.services)
|
||||
WelcomeView()
|
||||
.accessibilityElement()
|
||||
.accessibilityIdentifier("welcomeView")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue