From 6db80a522e2ea733ff22941ab62f05c5b6c0ce75 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 24 Feb 2022 16:02:20 -0800 Subject: [PATCH 1/5] extract chils view from share extension code --- .../Sources/Views/ShareExtensionView.swift | 80 +++++++++---------- 1 file changed, 38 insertions(+), 42 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift index d9d11893b..41a634fb2 100644 --- a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift @@ -106,15 +106,16 @@ struct CheckmarkButtonView: View { } } -public struct ShareExtensionView: View { - @State private var reminderTime: ReminderTime? - @State private var hideUntilReminded = false +public struct ShareExtensionChildView: View { + let debugText: String? + let title: String? + let status: ShareExtensionStatus + let onAppearAction: () -> Void + let readNowButtonAction: () -> Void + let dismissButtonTappedAction: (ReminderTime?, Bool) -> Void - @ObservedObject private var viewModel: ShareExtensionViewModel - - public init(viewModel: ShareExtensionViewModel) { - self.viewModel = viewModel - } + @Binding var reminderTime: ReminderTime? + @Binding var hideUntilReminded: Bool private var savedStateView: some View { HStack { @@ -123,7 +124,7 @@ public struct ShareExtensionView: View { title: "Read Now", systemIconName: "book", action: { - viewModel.performActionSubject.send(.readNowButtonTapped) + readNowButtonAction() } ) Spacer() @@ -144,12 +145,12 @@ public struct ShareExtensionView: View { public var body: some View { VStack(alignment: .leading) { #if DEBUG - if let debugText = viewModel.debugText { + if let debugText = debugText { Text(debugText) } #endif - if let title = viewModel.title { + if let title = title { Text(title) .font(.appHeadline) .lineLimit(1) @@ -159,7 +160,7 @@ public struct ShareExtensionView: View { Spacer() - if case ShareExtensionStatus.successfullySaved = viewModel.status { + if case ShareExtensionStatus.successfullySaved = status { if FeatureFlag.enableReadNowFromShareExtension { savedStateView } else { @@ -174,7 +175,7 @@ public struct ShareExtensionView: View { } .padding() } - } else if case let ShareExtensionStatus.failed(error) = viewModel.status { + } else if case let ShareExtensionStatus.failed(error) = status { HStack { Spacer() Text(error.displayMessage) @@ -230,11 +231,7 @@ public struct ShareExtensionView: View { Button( action: { - viewModel.performActionSubject - .send(.dismissButtonTapped( - reminderTime: reminderTime, - hideUntilReminded: hideUntilReminded - )) + dismissButtonTappedAction(reminderTime, hideUntilReminded) }, label: { Text("Dismiss") @@ -251,34 +248,33 @@ public struct ShareExtensionView: View { alignment: .topLeading ) .onAppear { - viewModel.performActionSubject.send(.savePage(requestID: viewModel.requestID)) + onAppearAction() } } } -#if DEBUG - struct ShareExtensionViewPreview: PreviewProvider { - public struct ContainerView: View { - let shareExtensionViewModel: ShareExtensionViewModel - @State var showExtensionModal = true +public struct ShareExtensionView: View { + @State private var reminderTime: ReminderTime? + @State private var hideUntilReminded = false - public var body: some View { - Button("Show Extension") { - showExtensionModal = true - } - .popover(isPresented: $showExtensionModal) { - ShareExtensionView(viewModel: shareExtensionViewModel) - } - } - } + @ObservedObject private var viewModel: ShareExtensionViewModel - static var previews: some View { - registerFonts() - - let viewModel = ShareExtensionViewModel() - viewModel.status = .successfullySaved - return ShareExtensionView(viewModel: viewModel) - .preferredColorScheme(.dark) - } + public init(viewModel: ShareExtensionViewModel) { + self.viewModel = viewModel } -#endif + + public var body: some View { + ShareExtensionChildView( + debugText: viewModel.debugText, + title: viewModel.title, + status: viewModel.status, + onAppearAction: { viewModel.performActionSubject.send(.savePage(requestID: viewModel.requestID)) }, + readNowButtonAction: { viewModel.performActionSubject.send(.readNowButtonTapped) }, + dismissButtonTappedAction: { + viewModel.performActionSubject.send(.dismissButtonTapped(reminderTime: $0, hideUntilReminded: $1)) + }, + reminderTime: $reminderTime, + hideUntilReminded: $hideUntilReminded + ) + } +} From 0ba2598fd0062d16b25181790bfcb96e89a7a375 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 24 Feb 2022 16:04:17 -0800 Subject: [PATCH 2/5] remove extra state vars from ShareExtensionView --- .../Sources/Views/ShareExtensionView.swift | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift index 41a634fb2..db590fbd8 100644 --- a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift @@ -114,8 +114,8 @@ public struct ShareExtensionChildView: View { let readNowButtonAction: () -> Void let dismissButtonTappedAction: (ReminderTime?, Bool) -> Void - @Binding var reminderTime: ReminderTime? - @Binding var hideUntilReminded: Bool + @State var reminderTime: ReminderTime? + @State var hideUntilReminded = false private var savedStateView: some View { HStack { @@ -254,9 +254,6 @@ public struct ShareExtensionChildView: View { } public struct ShareExtensionView: View { - @State private var reminderTime: ReminderTime? - @State private var hideUntilReminded = false - @ObservedObject private var viewModel: ShareExtensionViewModel public init(viewModel: ShareExtensionViewModel) { @@ -272,9 +269,7 @@ public struct ShareExtensionView: View { readNowButtonAction: { viewModel.performActionSubject.send(.readNowButtonTapped) }, dismissButtonTappedAction: { viewModel.performActionSubject.send(.dismissButtonTapped(reminderTime: $0, hideUntilReminded: $1)) - }, - reminderTime: $reminderTime, - hideUntilReminded: $hideUntilReminded + } ) } } From a5ff333ac4984a06684e00b89b79ed5d8896021c Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 24 Feb 2022 16:18:06 -0800 Subject: [PATCH 3/5] move share extension view model into App package --- .../Share/ShareExtensionScene.swift | 41 +++++++++++++ .../Sources/Views/ShareExtensionView.swift | 57 ++++++------------- 2 files changed, 57 insertions(+), 41 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift index 52e7326a5..f5ec1c934 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift @@ -20,6 +20,26 @@ public extension PlatformViewController { } } +public final class ShareExtensionViewModel: ObservableObject { + public enum Action { + case savePage(requestID: String) + case copyLinkButtonTapped + case readNowButtonTapped + case archiveButtonTapped + case dismissButtonTapped(reminderTime: ReminderTime?, hideUntilReminded: Bool) + } + + @Published public var title: String? + @Published public var status = ShareExtensionStatus.successfullySaved + @Published public var debugText: String? + + public var subscriptions = Set() + public let performActionSubject = PassthroughSubject() + public let requestID = UUID().uuidString.lowercased() + + public init() {} +} + extension ShareExtensionViewModel { static func make(extensionContext: NSExtensionContext?) -> ShareExtensionViewModel { let viewModel = ShareExtensionViewModel() @@ -100,3 +120,24 @@ extension ShareExtensionViewModel { .store(in: &subscriptions) } } + +public struct ShareExtensionView: View { + @ObservedObject private var viewModel: ShareExtensionViewModel + + public init(viewModel: ShareExtensionViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + ShareExtensionChildView( + debugText: viewModel.debugText, + title: viewModel.title, + status: viewModel.status, + onAppearAction: { viewModel.performActionSubject.send(.savePage(requestID: viewModel.requestID)) }, + readNowButtonAction: { viewModel.performActionSubject.send(.readNowButtonTapped) }, + dismissButtonTappedAction: { + viewModel.performActionSubject.send(.dismissButtonTapped(reminderTime: $0, hideUntilReminded: $1)) + } + ) + } +} diff --git a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift index db590fbd8..337e4472a 100644 --- a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift @@ -36,26 +36,6 @@ private extension SaveArticleError { } } -public final class ShareExtensionViewModel: ObservableObject { - public enum Action { - case savePage(requestID: String) - case copyLinkButtonTapped - case readNowButtonTapped - case archiveButtonTapped - case dismissButtonTapped(reminderTime: ReminderTime?, hideUntilReminded: Bool) - } - - @Published public var title: String? - @Published public var status = ShareExtensionStatus.successfullySaved - @Published public var debugText: String? - - public var subscriptions = Set() - public let performActionSubject = PassthroughSubject() - public let requestID = UUID().uuidString.lowercased() - - public init() {} -} - struct IconButtonView: View { let title: String let systemIconName: String @@ -114,6 +94,22 @@ public struct ShareExtensionChildView: View { let readNowButtonAction: () -> Void let dismissButtonTappedAction: (ReminderTime?, Bool) -> Void + public init( + debugText: String?, + title: String?, + status: ShareExtensionStatus, + onAppearAction: @escaping () -> Void, + readNowButtonAction: @escaping () -> Void, + dismissButtonTappedAction: @escaping (ReminderTime?, Bool) -> Void + ) { + self.debugText = debugText + self.title = title + self.status = status + self.onAppearAction = onAppearAction + self.readNowButtonAction = readNowButtonAction + self.dismissButtonTappedAction = dismissButtonTappedAction + } + @State var reminderTime: ReminderTime? @State var hideUntilReminded = false @@ -252,24 +248,3 @@ public struct ShareExtensionChildView: View { } } } - -public struct ShareExtensionView: View { - @ObservedObject private var viewModel: ShareExtensionViewModel - - public init(viewModel: ShareExtensionViewModel) { - self.viewModel = viewModel - } - - public var body: some View { - ShareExtensionChildView( - debugText: viewModel.debugText, - title: viewModel.title, - status: viewModel.status, - onAppearAction: { viewModel.performActionSubject.send(.savePage(requestID: viewModel.requestID)) }, - readNowButtonAction: { viewModel.performActionSubject.send(.readNowButtonTapped) }, - dismissButtonTappedAction: { - viewModel.performActionSubject.send(.dismissButtonTapped(reminderTime: $0, hideUntilReminded: $1)) - } - ) - } -} From 58cfc4f8b2753f0ac5679c9c96dde5bd3e98b231 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 24 Feb 2022 16:33:35 -0800 Subject: [PATCH 4/5] remove bing function from share extension view model --- .../Share/ShareExtensionScene.swift | 91 +++++++------------ 1 file changed, 32 insertions(+), 59 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift index f5ec1c934..dc91774f5 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift @@ -8,9 +8,9 @@ import Views public extension PlatformViewController { static func makeShareExtensionController(extensionContext: NSExtensionContext?) -> PlatformViewController { - let viewModel = ShareExtensionViewModel.make(extensionContext: extensionContext) - let rootView = ShareExtensionView(viewModel: viewModel) - let hostingController = PlatformHostingController(rootView: rootView) + let hostingController = PlatformHostingController( + rootView: ShareExtensionView(extensionContext: extensionContext) + ) #if os(iOS) hostingController.view.layer.cornerRadius = 12 hostingController.view.layer.masksToBounds = true @@ -20,62 +20,35 @@ public extension PlatformViewController { } } -public final class ShareExtensionViewModel: ObservableObject { - public enum Action { - case savePage(requestID: String) - case copyLinkButtonTapped - case readNowButtonTapped - case archiveButtonTapped - case dismissButtonTapped(reminderTime: ReminderTime?, hideUntilReminded: Bool) +final class ShareExtensionViewModel: ObservableObject { + let extensionContext: NSExtensionContext? + + @Published var title: String? + @Published var status = ShareExtensionStatus.successfullySaved + @Published var debugText: String? + + var subscriptions = Set() + let requestID = UUID().uuidString.lowercased() + + init(extensionContext: NSExtensionContext?) { + self.extensionContext = extensionContext } - @Published public var title: String? - @Published public var status = ShareExtensionStatus.successfullySaved - @Published public var debugText: String? - - public var subscriptions = Set() - public let performActionSubject = PassthroughSubject() - public let requestID = UUID().uuidString.lowercased() - - public init() {} -} - -extension ShareExtensionViewModel { - static func make(extensionContext: NSExtensionContext?) -> ShareExtensionViewModel { - let viewModel = ShareExtensionViewModel() - viewModel.bind(extensionContext: extensionContext) - return viewModel - } - - func bind(extensionContext: NSExtensionContext?) { - performActionSubject.sink { [weak self] action in - switch action { - case let .savePage(requestID): - self?.savePage(extensionContext: extensionContext, requestId: requestID) - case .dismissButtonTapped: - extensionContext?.completeRequest(returningItems: [], completionHandler: nil) - case .copyLinkButtonTapped: - print("copy link button tapped") - case .readNowButtonTapped: - #if os(iOS) - if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication { - let deepLinkUrl = NSURL(string: "omnivore://shareExtensionRequestID/\(self?.requestID ?? "")") - application.perform(NSSelectorFromString("openURL:"), with: deepLinkUrl) - } - #endif - extensionContext?.completeRequest(returningItems: [], completionHandler: nil) - case .archiveButtonTapped: - print("archive button tapped") + func handleReadNowAction() { + #if os(iOS) + if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication { + let deepLinkUrl = NSURL(string: "omnivore://shareExtensionRequestID/\(requestID)") + application.perform(NSSelectorFromString("openURL:"), with: deepLinkUrl) } - } - .store(in: &subscriptions) + #endif + extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } - private func savePage(extensionContext: NSExtensionContext?, requestId: String) { + func savePage() { PageScraper.scrape(extensionContext: extensionContext) { [weak self] result in switch result { case let .success(payload): - self?.persist(pageScrapePayload: payload, requestId: requestId) + self?.persist(pageScrapePayload: payload, requestId: self?.requestID ?? "") case let .failure(error): self?.debugText = error.message } @@ -121,22 +94,22 @@ extension ShareExtensionViewModel { } } -public struct ShareExtensionView: View { +struct ShareExtensionView: View { @ObservedObject private var viewModel: ShareExtensionViewModel - public init(viewModel: ShareExtensionViewModel) { - self.viewModel = viewModel + init(extensionContext: NSExtensionContext?) { + self.viewModel = ShareExtensionViewModel(extensionContext: extensionContext) } - public var body: some View { + var body: some View { ShareExtensionChildView( debugText: viewModel.debugText, title: viewModel.title, status: viewModel.status, - onAppearAction: { viewModel.performActionSubject.send(.savePage(requestID: viewModel.requestID)) }, - readNowButtonAction: { viewModel.performActionSubject.send(.readNowButtonTapped) }, - dismissButtonTappedAction: { - viewModel.performActionSubject.send(.dismissButtonTapped(reminderTime: $0, hideUntilReminded: $1)) + onAppearAction: viewModel.savePage, + readNowButtonAction: viewModel.handleReadNowAction, + dismissButtonTappedAction: { _, _ in + viewModel.extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } ) } From fee91c86a0d72e65cd68536c0f72aafd190bc2af Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 24 Feb 2022 17:09:47 -0800 Subject: [PATCH 5/5] move create profile view into views package --- .../Sources/App/RootViewModel.swift | 3 - .../Views/CreateProfileContainerView.swift | 126 ++++++++++ .../Sources/App/Views/CreateProfileView.swift | 231 ------------------ .../Sources/App/Views/RegistrationView.swift | 2 +- .../RegistrationViews/CreateProfileView.swift | 151 ++++++++++++ 5 files changed, 278 insertions(+), 235 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/Views/CreateProfileContainerView.swift delete mode 100644 apple/OmnivoreKit/Sources/App/Views/CreateProfileView.swift create mode 100644 apple/OmnivoreKit/Sources/Views/RegistrationViews/CreateProfileView.swift diff --git a/apple/OmnivoreKit/Sources/App/RootViewModel.swift b/apple/OmnivoreKit/Sources/App/RootViewModel.swift index 72ceaf526..6b50a6add 100644 --- a/apple/OmnivoreKit/Sources/App/RootViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/RootViewModel.swift @@ -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() - public let performActionSubject = PassthroughSubject() public init() { registerFonts() diff --git a/apple/OmnivoreKit/Sources/App/Views/CreateProfileContainerView.swift b/apple/OmnivoreKit/Sources/App/Views/CreateProfileContainerView.swift new file mode 100644 index 000000000..c9c21452c --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/CreateProfileContainerView.swift @@ -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() + + 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) } + ) + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/CreateProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/CreateProfileView.swift deleted file mode 100644 index 06f608c10..000000000 --- a/apple/OmnivoreKit/Sources/App/Views/CreateProfileView.swift +++ /dev/null @@ -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() - - 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 - } - } -} diff --git a/apple/OmnivoreKit/Sources/App/Views/RegistrationView.swift b/apple/OmnivoreKit/Sources/App/Views/RegistrationView.swift index aabb45de8..d358b28d0 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RegistrationView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/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 { - CreateProfileView(userProfile: userProfile, dataService: dataService) + CreateProfileContainerView(userProfile: userProfile, dataService: dataService) } else if case let RegistrationViewModel.RegistrationState.newAppleSignUp(userProfile) = registrationState { NewAppleSignupView( userProfile: userProfile, diff --git a/apple/OmnivoreKit/Sources/Views/RegistrationViews/CreateProfileView.swift b/apple/OmnivoreKit/Sources/Views/RegistrationViews/CreateProfileView.swift new file mode 100644 index 000000000..d573e47c9 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/RegistrationViews/CreateProfileView.swift @@ -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, + 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 + } + } +}