diff --git a/Riot/Modules/Camera/CameraPresenter.swift b/Riot/Modules/Camera/CameraPresenter.swift index 12373bee9..863115d23 100644 --- a/Riot/Modules/Camera/CameraPresenter.swift +++ b/Riot/Modules/Camera/CameraPresenter.swift @@ -19,7 +19,7 @@ import UIKit import AVFoundation @objc protocol CameraPresenterDelegate: AnyObject { - func cameraPresenter(_ presenter: CameraPresenter, didSelectImageData imageData: Data, withUTI uti: MXKUTI?) + func cameraPresenter(_ presenter: CameraPresenter, didSelectImage image: UIImage) func cameraPresenter(_ presenter: CameraPresenter, didSelectVideoAt url: URL) func cameraPresenterDidCancel(_ cameraPresenter: CameraPresenter) } @@ -27,12 +27,6 @@ import AVFoundation /// CameraPresenter enables to present native camera @objc final class CameraPresenter: NSObject { - // MARK: - Constants - - private enum Constants { - static let jpegCompressionQuality: CGFloat = 1.0 - } - // MARK: - Properties // MARK: Private @@ -131,8 +125,8 @@ extension CameraPresenter: UIImagePickerControllerDelegate { func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { if let videoURL = info[.mediaURL] as? URL { self.delegate?.cameraPresenter(self, didSelectVideoAt: videoURL) - } else if let image = (info[.editedImage] ?? info[.originalImage]) as? UIImage, let imageData = image.jpegData(compressionQuality: Constants.jpegCompressionQuality) { - self.delegate?.cameraPresenter(self, didSelectImageData: imageData, withUTI: MXKUTI.jpeg) + } else if let image = (info[.editedImage] ?? info[.originalImage]) as? UIImage { + self.delegate?.cameraPresenter(self, didSelectImage: image) } } diff --git a/Riot/Modules/MediaPicker/SingleImagePickerPresenter.swift b/Riot/Modules/MediaPicker/SingleImagePickerPresenter.swift index 1a1d20169..75135e8b2 100644 --- a/Riot/Modules/MediaPicker/SingleImagePickerPresenter.swift +++ b/Riot/Modules/MediaPicker/SingleImagePickerPresenter.swift @@ -27,6 +27,12 @@ import AVFoundation @objcMembers final class SingleImagePickerPresenter: NSObject { + // MARK: - Constants + + private enum Constants { + static let jpegCompressionQuality: CGFloat = 1.0 + } + // MARK: - Properties // MARK: Private @@ -117,8 +123,10 @@ final class SingleImagePickerPresenter: NSObject { // MARK: - CameraPresenterDelegate extension SingleImagePickerPresenter: CameraPresenterDelegate { - func cameraPresenter(_ cameraPresenter: CameraPresenter, didSelectImageData imageData: Data, withUTI uti: MXKUTI?) { - self.delegate?.singleImagePickerPresenter(self, didSelectImageData: imageData, withUTI: uti) + func cameraPresenter(_ cameraPresenter: CameraPresenter, didSelectImage image: UIImage) { + if let imageData = image.jpegData(compressionQuality: Constants.jpegCompressionQuality) { + self.delegate?.singleImagePickerPresenter(self, didSelectImageData: imageData, withUTI: MXKUTI.jpeg) + } } func cameraPresenterDidCancel(_ cameraPresenter: CameraPresenter) { diff --git a/Riot/Modules/Onboarding/OnboardingCoordinator.swift b/Riot/Modules/Onboarding/OnboardingCoordinator.swift index 52d977f0f..0a736cab1 100644 --- a/Riot/Modules/Onboarding/OnboardingCoordinator.swift +++ b/Riot/Modules/Onboarding/OnboardingCoordinator.swift @@ -251,8 +251,6 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { self.session = session self.authenticationType = authenticationType - // May need to move the spinner and key verification up to here in order to coordinate properly. - // Check whether another screen should be shown. if #available(iOS 14.0, *) { if authenticationType == .register, @@ -272,6 +270,10 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { completeIfReady() } + /// Checks the capabilities of the user's homeserver in order to determine + /// whether or not the display name and avatar can be updated. + /// + /// Once complete this method will start the post authentication flow automatically. @available(iOS 14.0, *) private func checkHomeserverCapabilities(for userSession: UserSession) { userSession.matrixSession.matrixRestClient.capabilities { [weak self] capabilities in @@ -308,11 +310,13 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { // MARK: - Post-Authentication + /// Starts the part of the flow that comes after authentication for new users. @available(iOS 14.0, *) private func beginPostAuthentication(for userSession: UserSession) { showCongratulationsScreen(for: userSession) } + /// Show the congratulations screen for new users. The screen will be configured based on the homeserver's capabilities. @available(iOS 14.0, *) private func showCongratulationsScreen(for userSession: UserSession) { MXLog.debug("[OnboardingCoordinator] showCongratulationsScreen") @@ -335,6 +339,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { } } + /// Displays the next view in the flow after the congratulations screen. @available(iOS 14.0, *) private func congratulationsCoordinator(_ coordinator: OnboardingCongratulationsCoordinator, didCompleteWith result: OnboardingCongratulationsCoordinatorResult) { switch result { @@ -360,6 +365,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { completeIfReady() } + /// Show the display name personalization screen for new users using the supplied user session. @available(iOS 14.0, *) private func showDisplayNameScreen(for userSession: UserSession) { MXLog.debug("[OnboardingCoordinator]: showDisplayNameScreen") @@ -380,6 +386,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { } } + /// Displays the next view in the flow after the display name screen. @available(iOS 14.0, *) private func displayNameCoordinator(_ coordinator: OnboardingDisplayNameCoordinator, didCompleteWith userSession: UserSession) { if shouldShowAvatarScreen { @@ -387,12 +394,14 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { return } else if Analytics.shared.shouldShowAnalyticsPrompt { showAnalyticsPrompt(for: userSession.matrixSession) + return } onboardingFinished = true completeIfReady() } + /// Show the avatar personalization screen for new users using the supplied user session. @available(iOS 14.0, *) private func showAvatarScreen(for userSession: UserSession) { MXLog.debug("[OnboardingCoordinator]: showAvatarScreen") @@ -408,8 +417,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { add(childCoordinator: coordinator) coordinator.start() - #warning("Should become root if display name was disabled.") - if navigationRouter.modules.isEmpty { + if navigationRouter.modules.isEmpty || !shouldShowDisplayNameScreen { navigationRouter.setRootModule(coordinator, hideNavigationBar: false, animated: true) { [weak self] in self?.remove(childCoordinator: coordinator) } @@ -420,6 +428,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { } } + /// Displays the next view in the flow after the avatar screen. @available(iOS 14.0, *) private func avatarCoordinator(_ coordinator: OnboardingAvatarCoordinator, didCompleteWith userSession: UserSession) { if Analytics.shared.shouldShowAnalyticsPrompt { @@ -431,6 +440,9 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { completeIfReady() } + /// Shows the analytics prompt for the supplied session. + /// + /// Check `Analytics.shared.shouldShowAnalyticsPrompt` before calling this method. @available(iOS 14.0, *) private func showAnalyticsPrompt(for session: MXSession) { MXLog.debug("[OnboardingCoordinator]: Invite the user to send analytics") @@ -452,6 +464,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { } } + /// Displays the next view in the flow after the analytics screen. private func analyticsPromptCoordinatorDidComplete(_ coordinator: AnalyticsPromptCoordinator) { onboardingFinished = true completeIfReady() @@ -459,6 +472,8 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { // MARK: - Finished + /// Calls the coordinator's completion handler if both `onboardingFinished` and `authenticationFinished` + /// are true. Otherwise displays any pending screens and waits to be called again. private func completeIfReady() { guard onboardingFinished else { MXLog.debug("[OnboardingCoordinator] Delaying onboarding completion until all screens have been shown.") diff --git a/Riot/Modules/PhotoPicker/PhotoPickerPresenter.swift b/Riot/Modules/PhotoPicker/PhotoPickerPresenter.swift index 87bfd2bb0..c4dde746f 100644 --- a/Riot/Modules/PhotoPicker/PhotoPickerPresenter.swift +++ b/Riot/Modules/PhotoPicker/PhotoPickerPresenter.swift @@ -34,7 +34,6 @@ final class PhotoPickerPresenter: NSObject { // MARK: Private private weak var pickerViewController: UIViewController? - private var filter: PHPickerFilter? private var indicatorPresenter: UserIndicatorTypePresenterProtocol? private var loadingIndicator: UserIndicator? @@ -45,6 +44,7 @@ final class PhotoPickerPresenter: NSObject { // MARK: - Public + // TODO: Support videos and multi-selection func presentPicker(from presentingViewController: UIViewController, with filter: PHPickerFilter?, animated: Bool) { var configuration = PHPickerConfiguration(photoLibrary: .shared()) configuration.selectionLimit = 1 @@ -71,7 +71,6 @@ final class PhotoPickerPresenter: NSObject { } func hideLoadingIndicator() { - loadingIndicator?.cancel() loadingIndicator = nil } } @@ -81,10 +80,7 @@ final class PhotoPickerPresenter: NSObject { extension PhotoPickerPresenter: PHPickerViewControllerDelegate { func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { // TODO: Handle videos and multi-selection - guard - let provider = results.first?.itemProvider, - provider.canLoadObject(ofClass: UIImage.self) - else { + guard let provider = results.first?.itemProvider, provider.canLoadObject(ofClass: UIImage.self) else { self.delegate?.photoPickerPresenterDidCancel(self) return } diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift index 1296d0903..56139ec08 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift @@ -104,13 +104,8 @@ final class OnboardingAvatarCoordinator: Coordinator, Presentable { waitingIndicator = indicatorPresenter.present(.loading(label: VectorL10n.saving, isInteractionBlocking: true)) } - private func stopWaiting(error: Error? = nil) { - waitingIndicator?.cancel() + private func stopWaiting() { waitingIndicator = nil - - if let error = error { - onboardingAvatarViewModel.update(with: error) - } } private func pickImage() { @@ -123,11 +118,6 @@ final class OnboardingAvatarCoordinator: Coordinator, Presentable { cameraPresenter.presentCamera(from: controller, with: [.image], animated: true) } - #warning("Temporary") - func unknownError() -> Error { - MXError(errorCode: "M.UNKNOWN", error: "Something went wrong!").createNSError() - } - func setAvatar(_ image: UIImage?) { guard let image = image else { MXLog.error("[OnboardingAvatarCoordinator] setAvatar called with a nil image.") @@ -138,7 +128,8 @@ final class OnboardingAvatarCoordinator: Coordinator, Presentable { guard let avatarData = MXKTools.forceImageOrientationUp(image)?.jpegData(compressionQuality: 0.5) else { MXLog.error("[OnboardingAvatarCoordinator] Failed to create jpeg data.") - self.stopWaiting(error: self.unknownError()) + self.stopWaiting() + self.onboardingAvatarViewModel.processError(nil) return } @@ -146,7 +137,9 @@ final class OnboardingAvatarCoordinator: Coordinator, Presentable { guard let self = self else { return } guard let urlString = urlString else { - self.stopWaiting(error: self.unknownError()) + MXLog.error("[OnboardingAvatarCoordinator] Missing URL string for avatar.") + self.stopWaiting() + self.onboardingAvatarViewModel.processError(nil) return } @@ -156,11 +149,13 @@ final class OnboardingAvatarCoordinator: Coordinator, Presentable { self.completion?(self.parameters.userSession) } failure: { [weak self] error in guard let self = self else { return } - self.stopWaiting(error: error ?? self.unknownError()) + self.stopWaiting() + self.onboardingAvatarViewModel.processError(error as NSError?) } } failure: { [weak self] error in guard let self = self else { return } - self.stopWaiting(error: error ?? self.unknownError()) + self.stopWaiting() + self.onboardingAvatarViewModel.processError(error as NSError?) } } } @@ -183,8 +178,8 @@ extension OnboardingAvatarCoordinator: PhotoPickerPresenterDelegate { @available(iOS 14.0, *) extension OnboardingAvatarCoordinator: CameraPresenterDelegate { - func cameraPresenter(_ presenter: CameraPresenter, didSelectImageData imageData: Data, withUTI uti: MXKUTI?) { - onboardingAvatarViewModel.updateAvatarImage(with: UIImage(data: imageData)) + func cameraPresenter(_ presenter: CameraPresenter, didSelectImage image: UIImage) { + onboardingAvatarViewModel.updateAvatarImage(with: image) presenter.dismiss(animated: true, completion: nil) } diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModel.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModel.swift index f1beeb33f..72376b34d 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModel.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModel.swift @@ -61,9 +61,7 @@ class OnboardingAvatarViewModel: OnboardingAvatarViewModelType, OnboardingAvatar state.avatar = image } - func update(with error: Error) { - if let error = error as NSError? { - state.bindings.alertInfo = AlertInfo(error: error) - } + func processError(_ error: NSError?) { + state.bindings.alertInfo = AlertInfo(error: error) } } diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModelProtocol.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModelProtocol.swift index fa16204b6..0e757a9dd 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModelProtocol.swift @@ -22,7 +22,10 @@ protocol OnboardingAvatarViewModelProtocol { @available(iOS 14, *) var context: OnboardingAvatarViewModelType.Context { get } + /// Update the view model to show the image that the user has picked. func updateAvatarImage(with image: UIImage?) - func update(with error: Error) + /// Update the view model to show that an error has occurred. + /// - Parameter error: The error to be displayed or `nil` to display a generic alert. + func processError(_ error: NSError?) } diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/Test/UI/OnboardingAvatarUITests.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/Test/UI/OnboardingAvatarUITests.swift index a77e77b39..f9cc94e70 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/Test/UI/OnboardingAvatarUITests.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/Test/UI/OnboardingAvatarUITests.swift @@ -50,6 +50,10 @@ class OnboardingAvatarUITests: MockScreenTest { let avatarImage = app.images["avatarImage"] XCTAssertFalse(avatarImage.exists, "The avatar image should be hidden as no selection has been made.") + + let saveButton = app.buttons["saveButton"] + XCTAssertTrue(saveButton.exists, "There should be a save button.") + XCTAssertFalse(saveButton.isEnabled, "The save button should not be enabled.") } func verifyUserSelectedAvatar() { @@ -58,5 +62,9 @@ class OnboardingAvatarUITests: MockScreenTest { let avatarImage = app.images["avatarImage"] XCTAssertTrue(avatarImage.exists, "The selected avatar should be shown.") + + let saveButton = app.buttons["saveButton"] + XCTAssertTrue(saveButton.exists, "There should be a save button.") + XCTAssertTrue(saveButton.isEnabled, "The save button should be enabled.") } } diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/View/OnboardingAvatarScreen.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/View/OnboardingAvatarScreen.swift index 8ef8cb9fd..faab7e89f 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/View/OnboardingAvatarScreen.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/View/OnboardingAvatarScreen.swift @@ -127,6 +127,7 @@ struct OnboardingAvatarScreen: View { } .buttonStyle(PrimaryActionButtonStyle()) .disabled(viewModel.viewState.avatar == nil) + .accessibilityIdentifier("saveButton") Button { viewModel.send(viewAction: .skip) } label: { Text(VectorL10n.onboardingPersonalizationSkip) diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/Coordinator/OnboardingCongratulationsCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/Coordinator/OnboardingCongratulationsCoordinator.swift index 78f8ea5be..e76d19313 100644 --- a/RiotSwiftUI/Modules/Onboarding/Congratulations/Coordinator/OnboardingCongratulationsCoordinator.swift +++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/Coordinator/OnboardingCongratulationsCoordinator.swift @@ -48,7 +48,7 @@ final class OnboardingCongratulationsCoordinator: Coordinator, Presentable { init(parameters: OnboardingCongratulationsCoordinatorParameters) { self.parameters = parameters - #warning("Add confetti when personalizationDisabled is false") + // TODO: Add confetti when personalizationDisabled is false let viewModel = OnboardingCongratulationsViewModel(userId: parameters.userSession.userId, personalizationDisabled: parameters.personalizationDisabled) let view = OnboardingCongratulationsScreen(viewModel: viewModel.context) diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/Coordinator/OnboardingDisplayNameCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/Coordinator/OnboardingDisplayNameCoordinator.swift index 94f992162..3f3be69f6 100644 --- a/RiotSwiftUI/Modules/Onboarding/DisplayName/Coordinator/OnboardingDisplayNameCoordinator.swift +++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/Coordinator/OnboardingDisplayNameCoordinator.swift @@ -84,13 +84,8 @@ final class OnboardingDisplayNameCoordinator: Coordinator, Presentable { waitingIndicator = indicatorPresenter.present(.loading(label: VectorL10n.saving, isInteractionBlocking: true)) } - private func stopWaiting(error: Error? = nil) { - waitingIndicator?.cancel() + private func stopWaiting() { waitingIndicator = nil - - if let error = error { - onboardingDisplayNameViewModel.update(with: error) - } } private func setDisplayName(_ displayName: String) { @@ -102,7 +97,8 @@ final class OnboardingDisplayNameCoordinator: Coordinator, Presentable { self.completion?(self.parameters.userSession) } failure: { [weak self] error in guard let self = self else { return } - self.stopWaiting(error: error) + self.stopWaiting() + self.onboardingDisplayNameViewModel.processError(error as NSError?) } } } diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModel.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModel.swift index e701cce08..e72b0b7be 100644 --- a/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModel.swift +++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModel.swift @@ -52,14 +52,13 @@ class OnboardingDisplayNameViewModel: OnboardingDisplayNameViewModelType, Onboar } } - func update(with error: Error) { - if let error = error as NSError? { - state.bindings.alertInfo = AlertInfo(error: error) - } + func processError(_ error: NSError?) { + state.bindings.alertInfo = AlertInfo(error: error) } // MARK: - Private + /// Checks for a display name that exceeds 256 characters and updates the footer error if needed. private func validateDisplayName() { if state.bindings.displayName.count > 256 { guard state.validationErrorMessage == nil else { return } diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModelProtocol.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModelProtocol.swift index e506ae1cd..d03c8416e 100644 --- a/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModelProtocol.swift @@ -22,5 +22,7 @@ protocol OnboardingDisplayNameViewModelProtocol { @available(iOS 14, *) var context: OnboardingDisplayNameViewModelType.Context { get } - func update(with error: Error) + /// Update the view model to show that an error has occurred. + /// - Parameter error: The error to be displayed or `nil` to display a generic alert. + func processError(_ error: NSError?) } diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/UI/OnboardingDisplayNameUITests.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/UI/OnboardingDisplayNameUITests.swift index 7ab52e933..30aa2ba50 100644 --- a/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/UI/OnboardingDisplayNameUITests.swift +++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/UI/OnboardingDisplayNameUITests.swift @@ -49,6 +49,10 @@ class OnboardingDisplayNameUITests: MockScreenTest { let footer = app.staticTexts["textFieldFooter"] XCTAssertTrue(footer.exists, "The textfield's footer should always be shown.") XCTAssertEqual(footer.label, VectorL10n.onboardingDisplayNameHint, "The footer should display a hint when no text is set.") + + let saveButton = app.buttons["saveButton"] + XCTAssertTrue(saveButton.exists, "There should be a save button.") + XCTAssertFalse(saveButton.isEnabled, "The save button should not be enabled.") } func verifyDisplayName(displayName: String) { @@ -57,6 +61,10 @@ class OnboardingDisplayNameUITests: MockScreenTest { XCTAssertEqual(textField.value as? String, displayName, "When a name has been set, it should show in the textfield.") XCTAssertEqual(textField.placeholderValue, VectorL10n.onboardingDisplayNamePlaceholder, "The textfield's placeholder should be set.") + let saveButton = app.buttons["saveButton"] + XCTAssertTrue(saveButton.exists, "There should be a save button.") + XCTAssertTrue(saveButton.isEnabled, "The save button should be enabled.") + let footer = app.staticTexts["textFieldFooter"] XCTAssertTrue(footer.exists, "The textfield's footer should always be shown.") XCTAssertEqual(footer.label, VectorL10n.onboardingDisplayNameHint, "The footer should display a hint when an acceptable name is entered.") @@ -71,5 +79,9 @@ class OnboardingDisplayNameUITests: MockScreenTest { let footer = app.staticTexts["textFieldFooter"] XCTAssertTrue(footer.exists, "The textfield's footer should always be shown.") XCTAssertEqual(footer.label, VectorL10n.onboardingDisplayNameMaxLength, "The footer should display an error when the display name is too long.") + + let saveButton = app.buttons["saveButton"] + XCTAssertTrue(saveButton.exists, "There should be a save button.") + XCTAssertFalse(saveButton.isEnabled, "The save button should not be enabled.") } } diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/Unit/OnboardingDisplayNameViewModelTests.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/Unit/OnboardingDisplayNameViewModelTests.swift index 765b31305..7f49a2aa2 100644 --- a/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/Unit/OnboardingDisplayNameViewModelTests.swift +++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/Unit/OnboardingDisplayNameViewModelTests.swift @@ -21,10 +21,6 @@ import Combine @available(iOS 14.0, *) class OnboardingDisplayNameViewModelTests: XCTestCase { - private enum Constants { - static let displayName = "Alice" - } - var viewModel: OnboardingDisplayNameViewModel! var context: OnboardingDisplayNameViewModelType.Context! diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/View/OnboardingDisplayNameScreen.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/View/OnboardingDisplayNameScreen.swift index 85e723fba..a678e1aa3 100644 --- a/RiotSwiftUI/Modules/Onboarding/DisplayName/View/OnboardingDisplayNameScreen.swift +++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/View/OnboardingDisplayNameScreen.swift @@ -111,7 +111,8 @@ struct OnboardingDisplayNameScreen: View { viewModel.send(viewAction: .save) } .buttonStyle(PrimaryActionButtonStyle()) - .disabled(viewModel.displayName.isEmpty) + .disabled(viewModel.displayName.isEmpty || viewModel.viewState.validationErrorMessage != nil) + .accessibilityIdentifier("saveButton") Button { viewModel.send(viewAction: .skip) } label: { Text(VectorL10n.onboardingPersonalizationSkip) diff --git a/changelog.d/5652.wip b/changelog.d/5652.wip new file mode 100644 index 000000000..0d173fbd5 --- /dev/null +++ b/changelog.d/5652.wip @@ -0,0 +1 @@ +Onboarding: Add screens for setting a display name and avatar when signing up for the first time. \ No newline at end of file