From 0cb8d1bf6e35606d6e41dbb4e4110aaa9fe94634 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Mon, 1 Mar 2021 15:40:40 +0800 Subject: [PATCH 1/6] chore: add error detail --- .../Entity/Mastodon+Entity+Error.swift | 4 +- .../Entity/Mastodon+Entity+ErrorDetail.swift | 101 ++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ErrorDetail.swift diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Error.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Error.swift index 9d5ae2a50..a36025745 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Error.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Error.swift @@ -19,10 +19,12 @@ extension Mastodon.Entity { public struct Error: Codable { public let error: String public let errorDescription: String? - + public let details: ErrorDetail? + enum CodingKeys: String, CodingKey { case error case errorDescription = "error_description" + case details } } } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ErrorDetail.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ErrorDetail.swift new file mode 100644 index 000000000..397475e0c --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ErrorDetail.swift @@ -0,0 +1,101 @@ +// +// Mastodon+Entity+ErrorDetail.swift +// +// +// Created by sxiaojian on 2021/3/1. +// + +import Foundation +extension Mastodon.Entity.Error { + /// ERR_BLOCKED When e-mail provider is not allowed + /// ERR_UNREACHABLE When e-mail address does not resolve to any IP via DNS (MX, A, AAAA) + /// ERR_TAKEN When username or e-mail are already taken + /// ERR_RESERVED When a username is reserved, e.g. "webmaster" or "admin" + /// ERR_ACCEPTED When agreement has not been accepted + /// ERR_BLANK When a required attribute is blank + /// ERR_INVALID When an attribute is malformed, e.g. wrong characters or invalid e-mail address + /// ERR_TOO_LONG When an attribute is over the character limit + /// ERR_INCLUSION When an attribute is not one of the allowed values, e.g. unsupported locale + public enum SignUpError: RawRepresentable, Codable { + case ERR_BLOCKED + case ERR_UNREACHABLE + case ERR_TAKEN + case ERR_RESERVED + case ERR_ACCEPTED + case ERR_BLANK + case ERR_INVALID + case ERR_TOO_LONG + case ERR_TOO_SHORT + case ERR_INCLUSION + case _other(String) + + public init?(rawValue: String) { + switch rawValue { + case "ERR_BLOCKED": self = .ERR_BLOCKED + case "ERR_UNREACHABLE": self = .ERR_UNREACHABLE + case "ERR_TAKEN": self = .ERR_TAKEN + case "ERR_RESERVED": self = .ERR_RESERVED + case "ERR_ACCEPTED": self = .ERR_ACCEPTED + case "ERR_BLANK": self = .ERR_BLANK + case "ERR_INVALID": self = .ERR_INVALID + case "ERR_TOO_LONG": self = .ERR_TOO_LONG + case "ERR_TOO_SHORT": self = .ERR_TOO_SHORT + case "ERR_INCLUSION": self = .ERR_INCLUSION + + default: + self = ._other(rawValue) + } + } + + public var rawValue: String { + switch self { + case .ERR_BLOCKED: return "ERR_BLOCKED" + case .ERR_UNREACHABLE: return "ERR_UNREACHABLE" + case .ERR_TAKEN: return "ERR_TAKEN" + case .ERR_RESERVED: return "ERR_RESERVED" + case .ERR_ACCEPTED: return "ERR_ACCEPTED" + case .ERR_BLANK: return "ERR_BLANK" + case .ERR_INVALID: return "ERR_INVALID" + case .ERR_TOO_LONG: return "ERR_TOO_LONG" + case .ERR_TOO_SHORT: return "ERR_TOO_SHORT" + case .ERR_INCLUSION: return "ERR_INCLUSION" + + case ._other(let value): return value + } + } + } +} + +public struct ErrorDetail: Codable { + public let username: [ErrorDetailReson]? + public let email: [ErrorDetailReson]? + public let password: [ErrorDetailReson]? + public let agreement: [ErrorDetailReson]? + public let locale: [ErrorDetailReson]? + public let reason: [ErrorDetailReson]? + + enum CodingKeys: String, CodingKey { + case username + case email + case password + case agreement + case locale + case reason + } +} + +public struct ErrorDetailReson: Codable { + public init(error: String, errorDescription: String?) { + self.error = Mastodon.Entity.Error.SignUpError(rawValue: error) ?? ._other(error) + self.errorDescription = errorDescription + } + + public let error: Mastodon.Entity.Error.SignUpError + public let errorDescription: String? + + + enum CodingKeys: String, CodingKey { + case error + case errorDescription = "description" + } +} From 732c5392d416303723902e9dd370353c43dd8df2 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Mon, 1 Mar 2021 16:27:24 +0800 Subject: [PATCH 2/6] chore: show error with 18n --- Localization/app.json | 20 ++++ Mastodon.xcodeproj/project.pbxproj | 4 + .../Mastodon+Entidy+ErrorDetailReson.swift | 94 +++++++++++++++++++ Mastodon/Extension/UIAlertController.swift | 42 ++++++++- Mastodon/Generated/Strings.swift | 36 +++++++ .../Resources/en.lproj/Localizable.strings | 18 +++- .../MastodonRegisterViewController.swift | 15 +++ .../MastodonServerRulesViewController.swift | 2 + .../Entity/Mastodon+Entity+ErrorDetail.swift | 61 ++++++------ 9 files changed, 260 insertions(+), 32 deletions(-) create mode 100644 Mastodon/Extension/Mastodon+Entidy+ErrorDetailReson.swift diff --git a/Localization/app.json b/Localization/app.json index e20e901db..a28b6e7f7 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -1,5 +1,25 @@ { "common": { + "errors": { + "item": { + "username": "username", + "email": "email", + "password": "password", + "agreement": "agreement", + "locale": "locale", + "reason": "reason" + }, + "ERR_BLOCKED": "is blocked", + "ERR_UNREACHABLE": "is unreachable", + "ERR_TAKEN": "is taken", + "ERR_RESERVED": "is reserved", + "ERR_ACCEPTED": "must be accepted", + "ERR_BLANK": "can't be blank", + "ERR_INVALID": "is invalid", + "ERR_TOO_LONG": "is too long", + "ERR_TOO_SHORT": "is too short", + "ERR_INCLUSION": "is inclusion" + }, "alerts": { "sign_up_failure": { "title": "Sign Up Failure" diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index e5429eeac..4b93b1f99 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -55,6 +55,7 @@ 2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */; }; 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; }; + 2D650FAB25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReson.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReson.swift */; }; 2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */; }; 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */; }; 2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */; }; @@ -260,6 +261,7 @@ 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewController+DebugAction.swift"; sourceTree = ""; }; 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Timeline.swift"; sourceTree = ""; }; 2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; + 2D650FAA25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReson.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entidy+ErrorDetailReson.swift"; sourceTree = ""; }; 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = ""; }; 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Toot.swift"; sourceTree = ""; }; 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewController.swift; sourceTree = ""; }; @@ -1007,6 +1009,7 @@ DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */, 2D32EAB925CB9B0500C9ED86 /* UIView.swift */, 0FAA101B25E10E760017CCDE /* UIFont.swift */, + 2D650FAA25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReson.swift */, ); path = Extension; sourceTree = ""; @@ -1502,6 +1505,7 @@ 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */, DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */, 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, + 2D650FAB25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReson.swift in Sources */, DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */, DB084B5725CBC56C00F898ED /* Toot.swift in Sources */, DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */, diff --git a/Mastodon/Extension/Mastodon+Entidy+ErrorDetailReson.swift b/Mastodon/Extension/Mastodon+Entidy+ErrorDetailReson.swift new file mode 100644 index 000000000..c78e4dfc4 --- /dev/null +++ b/Mastodon/Extension/Mastodon+Entidy+ErrorDetailReson.swift @@ -0,0 +1,94 @@ +// +// Mastodon+Entidy+ErrorDetailReason.swift +// Mastodon +// +// Created by sxiaojian on 2021/3/1. +// +import MastodonSDK + +extension Mastodon.Entity.ErrorDetailReason { + func localizedDescription() -> String { + switch self.error { + case .ERR_BLOCKED: + return L10n.Common.Errors.errBlocked + case .ERR_UNREACHABLE: + return L10n.Common.Errors.errUnreachable + case .ERR_TAKEN: + return L10n.Common.Errors.errTaken + case .ERR_RESERVED: + return L10n.Common.Errors.errReserved + case .ERR_ACCEPTED: + return L10n.Common.Errors.errAccepted + case .ERR_BLANK: + return L10n.Common.Errors.errBlank + case .ERR_INVALID: + return L10n.Common.Errors.errInvalid + case .ERR_TOO_LONG: + return L10n.Common.Errors.errTooLong + case .ERR_TOO_SHORT: + return L10n.Common.Errors.errTooShort + case .ERR_INCLUSION: + return L10n.Common.Errors.errInclusion + case ._other: + return self.errorDescription ?? "" + } + } +} + +extension Mastodon.Entity.ErrorDetail { + func localizedDescription() -> String { + var messages: [String?] = [] + if let username = self.username { + if !username.isEmpty { + let errors = username.map { + L10n.Common.Errors.Item.username + " " + $0.localizedDescription() + } + messages.append(contentsOf: errors) + } + } + if let email = self.email { + if !email.isEmpty { + let errors = email.map { + L10n.Common.Errors.Item.email + " " + $0.localizedDescription() + } + messages.append(contentsOf: errors) + } + } + if let password = self.password { + if !password.isEmpty { + let errors = password.map { + L10n.Common.Errors.Item.password + " " + $0.localizedDescription() + } + messages.append(contentsOf: errors) + } + } + if let agreement = self.agreement { + if !agreement.isEmpty { + let errors = agreement.map { + L10n.Common.Errors.Item.agreement + " " + $0.localizedDescription() + } + messages.append(contentsOf: errors) + } + } + if let locale = self.locale { + if !locale.isEmpty { + let errors = locale.map { + L10n.Common.Errors.Item.locale + " " + $0.localizedDescription() + } + messages.append(contentsOf: errors) + } + } + if let reason = self.reason { + if !reason.isEmpty { + let errors = reason.map { + L10n.Common.Errors.Item.reason + " " + $0.localizedDescription() + } + messages.append(contentsOf: errors) + } + } + let message = messages + .compactMap { $0 } + .joined(separator: ", ") + return message + } +} diff --git a/Mastodon/Extension/UIAlertController.swift b/Mastodon/Extension/UIAlertController.swift index 83c0ff555..755acc1ae 100644 --- a/Mastodon/Extension/UIAlertController.swift +++ b/Mastodon/Extension/UIAlertController.swift @@ -4,7 +4,7 @@ // import UIKit - +import MastodonSDK // Reference: // https://nshipster.com/swift-foundation-error-protocols/ extension UIAlertController { @@ -43,3 +43,43 @@ extension UIAlertController { } } +extension UIAlertController { + convenience init( + for error: Mastodon.API.Error, + title: String?, + preferredStyle: UIAlertController.Style + ) { + let _title: String + let message: String? + switch error.mastodonError { + case .generic(let mastodonEntityError): + + if let title = title { + _title = title + } else { + _title = error.errorDescription ?? "Error" + } + var messages: [String?] = [] + if let details = mastodonEntityError.details { + message = details.localizedDescription() + } else { + messages.append(contentsOf: [ + error.failureReason, + error.recoverySuggestion + ]) + message = messages + .compactMap { $0 } + .joined(separator: " ") + } + default: + _title = "Internal Error" + message = error.localizedDescription + } + + self.init( + title: _title, + message: message, + preferredStyle: preferredStyle + ) + } +} diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 47cbabab8..ce4ab38f8 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -82,6 +82,42 @@ internal enum L10n { internal static let single = L10n.tr("Localizable", "Common.Countable.Photo.Single") } } + internal enum Errors { + /// must be accepted + internal static let errAccepted = L10n.tr("Localizable", "Common.Errors.ErrAccepted") + /// can't be blank + internal static let errBlank = L10n.tr("Localizable", "Common.Errors.ErrBlank") + /// is blocked + internal static let errBlocked = L10n.tr("Localizable", "Common.Errors.ErrBlocked") + /// is inclusion + internal static let errInclusion = L10n.tr("Localizable", "Common.Errors.ErrInclusion") + /// is invalid + internal static let errInvalid = L10n.tr("Localizable", "Common.Errors.ErrInvalid") + /// is reserved + internal static let errReserved = L10n.tr("Localizable", "Common.Errors.ErrReserved") + /// is taken + internal static let errTaken = L10n.tr("Localizable", "Common.Errors.ErrTaken") + /// is too long + internal static let errTooLong = L10n.tr("Localizable", "Common.Errors.ErrTooLong") + /// is too short + internal static let errTooShort = L10n.tr("Localizable", "Common.Errors.ErrTooShort") + /// is unreachable + internal static let errUnreachable = L10n.tr("Localizable", "Common.Errors.ErrUnreachable") + internal enum Item { + /// agreement + internal static let agreement = L10n.tr("Localizable", "Common.Errors.Item.Agreement") + /// email + internal static let email = L10n.tr("Localizable", "Common.Errors.Item.Email") + /// locale + internal static let locale = L10n.tr("Localizable", "Common.Errors.Item.Locale") + /// password + internal static let password = L10n.tr("Localizable", "Common.Errors.Item.Password") + /// reason + internal static let reason = L10n.tr("Localizable", "Common.Errors.Item.Reason") + /// username + internal static let username = L10n.tr("Localizable", "Common.Errors.Item.Username") + } + } } internal enum Scene { diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index b3df9a77f..663ad25ad 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -23,6 +23,22 @@ "Common.Controls.Timeline.LoadMore" = "Load More"; "Common.Countable.Photo.Multiple" = "photos"; "Common.Countable.Photo.Single" = "photo"; +"Common.Errors.ErrAccepted" = "must be accepted"; +"Common.Errors.ErrBlank" = "can't be blank"; +"Common.Errors.ErrBlocked" = "is blocked"; +"Common.Errors.ErrInclusion" = "is inclusion"; +"Common.Errors.ErrInvalid" = "is invalid"; +"Common.Errors.ErrReserved" = "is reserved"; +"Common.Errors.ErrTaken" = "is taken"; +"Common.Errors.ErrTooLong" = "is too long"; +"Common.Errors.ErrTooShort" = "is too short"; +"Common.Errors.ErrUnreachable" = "is unreachable"; +"Common.Errors.Item.Agreement" = "agreement"; +"Common.Errors.Item.Email" = "email"; +"Common.Errors.Item.Locale" = "locale"; +"Common.Errors.Item.Password" = "password"; +"Common.Errors.Item.Reason" = "reason"; +"Common.Errors.Item.Username" = "username"; "Scene.ConfirmEmail.Button.DontReceiveEmail" = "I never got an email"; "Scene.ConfirmEmail.Button.OpenEmailApp" = "Open Email App"; "Scene.ConfirmEmail.DontReceiveEmail.Description" = "Check if your email address is correct as well as your junk folder if you haven’t."; @@ -62,4 +78,4 @@ any server."; "Scene.ServerRules.Subtitle" = "These rules are set by the admins of %@."; "Scene.ServerRules.Title" = "Some ground rules."; "Scene.Welcome.Slogan" = "Social networking -back in your hands."; +back in your hands."; \ No newline at end of file diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift index ff979c3dd..9bdaafd03 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift @@ -105,6 +105,20 @@ final class MastodonRegisterViewController: UIViewController, NeedsDependency, O let usernameIsTakenLabel: UILabel = { let label = UILabel() + let color = Asset.Colors.lightDangerRed.color + let font = UIFont.preferredFont(forTextStyle: .caption1) + let attributeString = NSMutableAttributedString() + + let errorImage = NSTextAttachment() + let configuration = UIImage.SymbolConfiguration(font: font) + errorImage.image = UIImage(systemName: "xmark.octagon.fill", withConfiguration: configuration)?.withTintColor(color) + let errorImageAttachment = NSAttributedString(attachment: errorImage) + attributeString.append(errorImageAttachment) + + let errorString = NSAttributedString(string: L10n.Common.Errors.Item.username + " " + L10n.Common.Errors.errTaken, attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) + attributeString.append(errorString) + label.attributedText = attributeString + return label }() @@ -392,6 +406,7 @@ extension MastodonRegisterViewController { .receive(on: DispatchQueue.main) .sink { [weak self] error in guard let self = self else { return } + guard let error = error as? Mastodon.API.Error else { return } let alertController = UIAlertController(for: error, title: "Sign Up Failure", preferredStyle: .alert) let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) alertController.addAction(okAction) diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift index cc7992e21..3886bda7c 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift @@ -8,6 +8,7 @@ import os.log import UIKit import Combine +import MastodonSDK final class MastodonServerRulesViewController: UIViewController, NeedsDependency { @@ -162,6 +163,7 @@ extension MastodonServerRulesViewController { .receive(on: DispatchQueue.main) .sink { [weak self] error in guard let self = self else { return } + guard let error = error as? Mastodon.API.Error else { return } let alertController = UIAlertController(for: error, title: "Sign Up Failure", preferredStyle: .alert) let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) alertController.addAction(okAction) diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ErrorDetail.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ErrorDetail.swift index 397475e0c..4d90779bb 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ErrorDetail.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ErrorDetail.swift @@ -65,37 +65,38 @@ extension Mastodon.Entity.Error { } } } +extension Mastodon.Entity { + public struct ErrorDetail: Codable { + public let username: [ErrorDetailReason]? + public let email: [ErrorDetailReason]? + public let password: [ErrorDetailReason]? + public let agreement: [ErrorDetailReason]? + public let locale: [ErrorDetailReason]? + public let reason: [ErrorDetailReason]? -public struct ErrorDetail: Codable { - public let username: [ErrorDetailReson]? - public let email: [ErrorDetailReson]? - public let password: [ErrorDetailReson]? - public let agreement: [ErrorDetailReson]? - public let locale: [ErrorDetailReson]? - public let reason: [ErrorDetailReson]? + enum CodingKeys: String, CodingKey { + case username + case email + case password + case agreement + case locale + case reason + } + } - enum CodingKeys: String, CodingKey { - case username - case email - case password - case agreement - case locale - case reason - } -} - -public struct ErrorDetailReson: Codable { - public init(error: String, errorDescription: String?) { - self.error = Mastodon.Entity.Error.SignUpError(rawValue: error) ?? ._other(error) - self.errorDescription = errorDescription - } - - public let error: Mastodon.Entity.Error.SignUpError - public let errorDescription: String? - - - enum CodingKeys: String, CodingKey { - case error - case errorDescription = "description" + public struct ErrorDetailReason: Codable { + public init(error: String, errorDescription: String?) { + self.error = Mastodon.Entity.Error.SignUpError(rawValue: error) ?? ._other(error) + self.errorDescription = errorDescription + } + + public let error: Mastodon.Entity.Error.SignUpError + public let errorDescription: String? + + + enum CodingKeys: String, CodingKey { + case error + case errorDescription = "description" + } } } From a659b35577511db24d2851d18dcb4689942349db Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Mon, 1 Mar 2021 17:45:02 +0800 Subject: [PATCH 3/6] chore: display UI when username is taken error return by sign up --- .../MastodonRegisterViewController.swift | 43 +++++++++++++++---- .../Register/MastodonRegisterViewModel.swift | 2 + 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift index 9bdaafd03..2f92e44cc 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift @@ -241,11 +241,12 @@ extension MastodonRegisterViewController { stackView.addArrangedSubview(largeTitleLabel) stackView.addArrangedSubview(photoView) stackView.addArrangedSubview(usernameTextField) + stackView.addArrangedSubview(usernameIsTakenLabel) stackView.addArrangedSubview(displayNameTextField) stackView.addArrangedSubview(emailTextField) stackView.addArrangedSubview(passwordTextField) stackView.addArrangedSubview(passwordCheckLabel) - if self.viewModel.approvalRequired { + if viewModel.approvalRequired { stackView.addArrangedSubview(inviteTextField) } // scrollView @@ -389,24 +390,48 @@ extension MastodonRegisterViewController { guard let self = self else { return } self.setTextFieldValidAppearance(self.passwordTextField, validateState: validateState) self.passwordCheckLabel.attributedText = self.viewModel.attributeStringForPassword(eightCharacters: validateState == .valid) - } .store(in: &disposeBag) viewModel.isAllValid - .receive(on: DispatchQueue.main) - .sink { [weak self] isAllValid in - guard let self = self else { return } - self.signUpButton.isEnabled = isAllValid - } - .store(in: &disposeBag) + .receive(on: DispatchQueue.main) + .sink { [weak self] isAllValid in + guard let self = self else { return } + self.signUpButton.isEnabled = isAllValid + } + .store(in: &disposeBag) + viewModel.isUsernameTaken + .receive(on: DispatchQueue.main) + .sink {[weak self] isUsernameTaken in + guard let self = self else { return } + if isUsernameTaken { + self.usernameIsTakenLabel.isHidden = false + stackView.setCustomSpacing(6, after: self.usernameTextField) + stackView.setCustomSpacing(16, after: self.usernameIsTakenLabel) + } else { + self.usernameIsTakenLabel.isHidden = true + stackView.setCustomSpacing(40, after: self.usernameTextField) + } + } + .store(in: &disposeBag) viewModel.error .compactMap { $0 } .receive(on: DispatchQueue.main) .sink { [weak self] error in guard let self = self else { return } guard let error = error as? Mastodon.API.Error else { return } + switch error.mastodonError { + case .generic(let mastodonEntityError): + if let usernameTakenError = mastodonEntityError.details?.username { + let isUsernameAvaliable = usernameTakenError.filter { errorDetailReason -> Bool in + errorDetailReason.error == .ERR_TAKEN + }.isEmpty + self.viewModel.isUsernameTaken.value = !isUsernameAvaliable + } + default: + break + } let alertController = UIAlertController(for: error, title: "Sign Up Failure", preferredStyle: .alert) let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) alertController.addAction(okAction) @@ -454,7 +479,7 @@ extension MastodonRegisterViewController { } .store(in: &disposeBag) - if self.viewModel.approvalRequired { + if viewModel.approvalRequired { inviteTextField.delegate = self NSLayoutConstraint.activate([ diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift index 5a9098347..8f930771a 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift @@ -34,6 +34,8 @@ final class MastodonRegisterViewModel { let passwordValidateState = CurrentValueSubject(.empty) let inviteValidateState = CurrentValueSubject(.empty) + let isUsernameTaken = CurrentValueSubject(false) + let isRegistering = CurrentValueSubject(false) let isAllValid = CurrentValueSubject(false) let error = CurrentValueSubject(nil) From 77a0708e7dd81ce30ed8fbb9ffe630ebfe0a378b Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Mon, 1 Mar 2021 18:06:37 +0800 Subject: [PATCH 4/6] chore: Make the server rules display before the sign-up form scene --- .../MastodonPickServerViewController.swift | 26 +++++--- .../MastodonRegisterViewController.swift | 60 +++++++------------ .../MastodonServerRulesViewController.swift | 54 +---------------- .../MastodonServerRulesViewModel.swift | 21 +++---- 4 files changed, 51 insertions(+), 110 deletions(-) diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index 9e10cd329..7aef5817e 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -265,13 +265,25 @@ extension MastodonPickServerViewController { } } receiveValue: { [weak self] response in guard let self = self else { return } - let mastodonRegisterViewModel = MastodonRegisterViewModel( - domain: server.domain, - authenticateInfo: response.authenticateInfo, - instance: response.instance.value, - applicationToken: response.applicationToken.value - ) - self.coordinator.present(scene: .mastodonRegister(viewModel: mastodonRegisterViewModel), from: nil, transition: .show) + if let rules = response.instance.value.rules, !rules.isEmpty { + // show server rules before register + let mastodonServerRulesViewModel = MastodonServerRulesViewModel( + domain: server.domain, + authenticateInfo: response.authenticateInfo, + rules: rules, + instance: response.instance.value, + applicationToken: response.applicationToken.value + ) + self.coordinator.present(scene: .mastodonServerRules(viewModel: mastodonServerRulesViewModel), from: self, transition: .show) + } else { + let mastodonRegisterViewModel = MastodonRegisterViewModel( + domain: server.domain, + authenticateInfo: response.authenticateInfo, + instance: response.instance.value, + applicationToken: response.applicationToken.value + ) + self.coordinator.present(scene: .mastodonRegister(viewModel: mastodonRegisterViewModel), from: nil, transition: .show) + } } .store(in: &disposeBag) } diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift index 2f92e44cc..9931a8b19 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift @@ -576,45 +576,29 @@ extension MastodonRegisterViewController { locale: "en" // TODO: ) - if let rules = viewModel.instance.rules, !rules.isEmpty { - // show server rules before register - let mastodonServerRulesViewModel = MastodonServerRulesViewModel( - context: context, - domain: viewModel.domain, - authenticateInfo: viewModel.authenticateInfo, - rules: rules, - registerQuery: query, - applicationAuthorization: viewModel.applicationAuthorization - ) - - viewModel.isRegistering.value = false - view.endEditing(true) - coordinator.present(scene: .mastodonServerRules(viewModel: mastodonServerRulesViewModel), from: self, transition: .show) - return - } else { - // register without show server rules - context.apiService.accountRegister( - domain: viewModel.domain, - query: query, - authorization: viewModel.applicationAuthorization - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] completion in - guard let self = self else { return } - self.viewModel.isRegistering.value = false - switch completion { - case .failure(let error): - self.viewModel.error.send(error) - case .finished: - break - } - } receiveValue: { [weak self] response in - guard let self = self else { return } - let userToken = response.value - let viewModel = MastodonConfirmEmailViewModel(context: self.context, email: email, authenticateInfo: self.viewModel.authenticateInfo, userToken: userToken) - self.coordinator.present(scene: .mastodonConfirmEmail(viewModel: viewModel), from: self, transition: .show) + // register without show server rules + context.apiService.accountRegister( + domain: viewModel.domain, + query: query, + authorization: viewModel.applicationAuthorization + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] completion in + guard let self = self else { return } + self.viewModel.isRegistering.value = false + switch completion { + case .failure(let error): + self.viewModel.error.send(error) + case .finished: + break } - .store(in: &disposeBag) + } receiveValue: { [weak self] response in + guard let self = self else { return } + let userToken = response.value + let viewModel = MastodonConfirmEmailViewModel(context: self.context, email: email, authenticateInfo: self.viewModel.authenticateInfo, userToken: userToken) + self.coordinator.present(scene: .mastodonConfirmEmail(viewModel: viewModel), from: self, transition: .show) } + .store(in: &disposeBag) + } } diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift index 3886bda7c..467239b87 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewController.swift @@ -8,7 +8,6 @@ import os.log import UIKit import Combine -import MastodonSDK final class MastodonServerRulesViewController: UIViewController, NeedsDependency { @@ -149,31 +148,6 @@ extension MastodonServerRulesViewController { rulesLabel.attributedText = viewModel.rulesAttributedString confirmButton.addTarget(self, action: #selector(MastodonServerRulesViewController.confirmButtonPressed(_:)), for: .touchUpInside) - - viewModel.isRegistering - .receive(on: DispatchQueue.main) - .sink { [weak self] isRegistering in - guard let self = self else { return } - isRegistering ? self.confirmButton.showLoading() : self.confirmButton.stopLoading() - } - .store(in: &disposeBag) - - viewModel.error - .compactMap { $0 } - .receive(on: DispatchQueue.main) - .sink { [weak self] error in - guard let self = self else { return } - guard let error = error as? Mastodon.API.Error else { return } - let alertController = UIAlertController(for: error, title: "Sign Up Failure", preferredStyle: .alert) - let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) - alertController.addAction(okAction) - self.coordinator.present( - scene: .alertController(alertController: alertController), - from: nil, - transition: .alertController(animated: true, completion: nil) - ) - } - .store(in: &disposeBag) } override func viewDidLayoutSubviews() { @@ -199,31 +173,9 @@ extension MastodonServerRulesViewController { extension MastodonServerRulesViewController { @objc private func confirmButtonPressed(_ sender: UIButton) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - - let email = viewModel.registerQuery.email - - context.apiService.accountRegister( - domain: viewModel.domain, - query: viewModel.registerQuery, - authorization: viewModel.applicationAuthorization - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] completion in - guard let self = self else { return } - self.viewModel.isRegistering.value = false - switch completion { - case .failure(let error): - self.viewModel.error.send(error) - case .finished: - break - } - } receiveValue: { [weak self] response in - guard let self = self else { return } - let userToken = response.value - let viewModel = MastodonConfirmEmailViewModel(context: self.context, email: email, authenticateInfo: self.viewModel.authenticateInfo, userToken: userToken) - self.coordinator.present(scene: .mastodonConfirmEmail(viewModel: viewModel), from: self, transition: .show) - } - .store(in: &disposeBag) + + let viewModel = MastodonRegisterViewModel(domain: self.viewModel.domain, authenticateInfo: self.viewModel.authenticateInfo, instance: self.viewModel.instance, applicationToken: self.viewModel.applicationToken) + self.coordinator.present(scene: .mastodonRegister(viewModel: viewModel), from: self, transition: .show) } } diff --git a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift index 9569ffe81..89f31bbc2 100644 --- a/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift +++ b/Mastodon/Scene/Onboarding/ServerRules/MastodonServerRulesViewModel.swift @@ -10,34 +10,27 @@ import Combine import MastodonSDK final class MastodonServerRulesViewModel { - // input - let context: AppContext + let domain: String let authenticateInfo: AuthenticationViewModel.AuthenticateInfo let rules: [Mastodon.Entity.Instance.Rule] - let registerQuery: Mastodon.API.Account.RegisterQuery - let applicationAuthorization: Mastodon.API.OAuth.Authorization - - // output - let isRegistering = CurrentValueSubject(false) - let error = CurrentValueSubject(nil) + let instance: Mastodon.Entity.Instance + let applicationToken: Mastodon.Entity.Token init( - context: AppContext, domain: String, authenticateInfo: AuthenticationViewModel.AuthenticateInfo, rules: [Mastodon.Entity.Instance.Rule], - registerQuery: Mastodon.API.Account.RegisterQuery, - applicationAuthorization: Mastodon.API.OAuth.Authorization + instance: Mastodon.Entity.Instance, + applicationToken: Mastodon.Entity.Token ) { - self.context = context self.domain = domain self.authenticateInfo = authenticateInfo self.rules = rules - self.registerQuery = registerQuery - self.applicationAuthorization = applicationAuthorization + self.instance = instance + self.applicationToken = applicationToken } var rulesAttributedString: NSAttributedString { From 148a996129726fd8ad0cb19dd109c4e9ebf6df0e Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Mon, 1 Mar 2021 19:19:51 +0800 Subject: [PATCH 5/6] chore: update the i18n suggests --- Localization/app.json | 20 +++--- .../Mastodon+Entidy+ErrorDetailReson.swift | 66 +++++++++---------- Mastodon/Generated/Strings.swift | 22 ++++--- .../Resources/en.lproj/Localizable.strings | 18 ++--- .../Register/MastodonRegisterViewModel.swift | 2 +- .../Entity/Mastodon+Entity+ErrorDetail.swift | 18 ++--- 6 files changed, 78 insertions(+), 68 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index a28b6e7f7..b5d375a15 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -9,16 +9,20 @@ "locale": "locale", "reason": "reason" }, - "ERR_BLOCKED": "is blocked", - "ERR_UNREACHABLE": "is unreachable", - "ERR_TAKEN": "is taken", - "ERR_RESERVED": "is reserved", + "itemDetail": { + "emailInvalid": "It's not a valid e-mail address", + "usernameInvalid": "username only contains alphanumeric characters and underscores" + }, + "ERR_BLOCKED": "contains a disallowed e-mail provider", + "ERR_UNREACHABLE": "does not seem to exist", + "ERR_TAKEN": "is already in use", + "ERR_RESERVED": "is a reserved keyword or username", "ERR_ACCEPTED": "must be accepted", - "ERR_BLANK": "can't be blank", + "ERR_BLANK": "is required", "ERR_INVALID": "is invalid", - "ERR_TOO_LONG": "is too long", - "ERR_TOO_SHORT": "is too short", - "ERR_INCLUSION": "is inclusion" + "ERR_TOO_LONG": "is too long ( can't be longer than 30 characters)", + "ERR_TOO_SHORT": "is too short (must be at least 8 characters)", + "ERR_INCLUSION": "is not a supported value" }, "alerts": { "sign_up_failure": { diff --git a/Mastodon/Extension/Mastodon+Entidy+ErrorDetailReson.swift b/Mastodon/Extension/Mastodon+Entidy+ErrorDetailReson.swift index c78e4dfc4..e72e2771f 100644 --- a/Mastodon/Extension/Mastodon+Entidy+ErrorDetailReson.swift +++ b/Mastodon/Extension/Mastodon+Entidy+ErrorDetailReson.swift @@ -38,53 +38,51 @@ extension Mastodon.Entity.ErrorDetailReason { extension Mastodon.Entity.ErrorDetail { func localizedDescription() -> String { var messages: [String?] = [] - if let username = self.username { - if !username.isEmpty { - let errors = username.map { - L10n.Common.Errors.Item.username + " " + $0.localizedDescription() + + if let username = self.username, !username.isEmpty { + let errors = username.map { errorDetailReason -> String in + if errorDetailReason.error == .ERR_INVALID { + return L10n.Common.Errors.Itemdetail.usernameinvalid + } else { + return L10n.Common.Errors.Item.username + " " + errorDetailReason.localizedDescription() } - messages.append(contentsOf: errors) } + messages.append(contentsOf: errors) } - if let email = self.email { - if !email.isEmpty { - let errors = email.map { - L10n.Common.Errors.Item.email + " " + $0.localizedDescription() + + if let email = self.email, !email.isEmpty { + let errors = email.map { errorDetailReason -> String in + if errorDetailReason.error == .ERR_INVALID { + return L10n.Common.Errors.Itemdetail.emailinvalid + } else { + return L10n.Common.Errors.Item.email + " " + errorDetailReason.localizedDescription() } - messages.append(contentsOf: errors) } + messages.append(contentsOf: errors) } - if let password = self.password { - if !password.isEmpty { - let errors = password.map { - L10n.Common.Errors.Item.password + " " + $0.localizedDescription() - } - messages.append(contentsOf: errors) + if let password = self.password,!password.isEmpty { + let errors = password.map { + L10n.Common.Errors.Item.password + " " + $0.localizedDescription() } + messages.append(contentsOf: errors) } - if let agreement = self.agreement { - if !agreement.isEmpty { - let errors = agreement.map { - L10n.Common.Errors.Item.agreement + " " + $0.localizedDescription() - } - messages.append(contentsOf: errors) + if let agreement = self.agreement, !agreement.isEmpty { + let errors = agreement.map { + L10n.Common.Errors.Item.agreement + " " + $0.localizedDescription() } + messages.append(contentsOf: errors) } - if let locale = self.locale { - if !locale.isEmpty { - let errors = locale.map { - L10n.Common.Errors.Item.locale + " " + $0.localizedDescription() - } - messages.append(contentsOf: errors) + if let locale = self.locale, !locale.isEmpty { + let errors = locale.map { + L10n.Common.Errors.Item.locale + " " + $0.localizedDescription() } + messages.append(contentsOf: errors) } - if let reason = self.reason { - if !reason.isEmpty { - let errors = reason.map { - L10n.Common.Errors.Item.reason + " " + $0.localizedDescription() - } - messages.append(contentsOf: errors) + if let reason = self.reason, !reason.isEmpty { + let errors = reason.map { + L10n.Common.Errors.Item.reason + " " + $0.localizedDescription() } + messages.append(contentsOf: errors) } let message = messages .compactMap { $0 } diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index ce4ab38f8..d9e7de9e5 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -85,23 +85,23 @@ internal enum L10n { internal enum Errors { /// must be accepted internal static let errAccepted = L10n.tr("Localizable", "Common.Errors.ErrAccepted") - /// can't be blank + /// is required internal static let errBlank = L10n.tr("Localizable", "Common.Errors.ErrBlank") - /// is blocked + /// contains a disallowed e-mail provider internal static let errBlocked = L10n.tr("Localizable", "Common.Errors.ErrBlocked") - /// is inclusion + /// is not a supported value internal static let errInclusion = L10n.tr("Localizable", "Common.Errors.ErrInclusion") /// is invalid internal static let errInvalid = L10n.tr("Localizable", "Common.Errors.ErrInvalid") - /// is reserved + /// is a reserved keyword or username internal static let errReserved = L10n.tr("Localizable", "Common.Errors.ErrReserved") - /// is taken + /// is already in use internal static let errTaken = L10n.tr("Localizable", "Common.Errors.ErrTaken") - /// is too long + /// is too long ( can't be longer than 30 characters) internal static let errTooLong = L10n.tr("Localizable", "Common.Errors.ErrTooLong") - /// is too short + /// is too short (must be at least 8 characters) internal static let errTooShort = L10n.tr("Localizable", "Common.Errors.ErrTooShort") - /// is unreachable + /// does not seem to exist internal static let errUnreachable = L10n.tr("Localizable", "Common.Errors.ErrUnreachable") internal enum Item { /// agreement @@ -117,6 +117,12 @@ internal enum L10n { /// username internal static let username = L10n.tr("Localizable", "Common.Errors.Item.Username") } + internal enum Itemdetail { + /// It's not a valid e-mail address + internal static let emailinvalid = L10n.tr("Localizable", "Common.Errors.Itemdetail.Emailinvalid") + /// username only contains alphanumeric characters and underscores + internal static let usernameinvalid = L10n.tr("Localizable", "Common.Errors.Itemdetail.Usernameinvalid") + } } } diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 663ad25ad..d127a93a3 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -24,21 +24,23 @@ "Common.Countable.Photo.Multiple" = "photos"; "Common.Countable.Photo.Single" = "photo"; "Common.Errors.ErrAccepted" = "must be accepted"; -"Common.Errors.ErrBlank" = "can't be blank"; -"Common.Errors.ErrBlocked" = "is blocked"; -"Common.Errors.ErrInclusion" = "is inclusion"; +"Common.Errors.ErrBlank" = "is required"; +"Common.Errors.ErrBlocked" = "contains a disallowed e-mail provider"; +"Common.Errors.ErrInclusion" = "is not a supported value"; "Common.Errors.ErrInvalid" = "is invalid"; -"Common.Errors.ErrReserved" = "is reserved"; -"Common.Errors.ErrTaken" = "is taken"; -"Common.Errors.ErrTooLong" = "is too long"; -"Common.Errors.ErrTooShort" = "is too short"; -"Common.Errors.ErrUnreachable" = "is unreachable"; +"Common.Errors.ErrReserved" = "is a reserved keyword or username"; +"Common.Errors.ErrTaken" = "is already in use"; +"Common.Errors.ErrTooLong" = "is too long ( can't be longer than 30 characters)"; +"Common.Errors.ErrTooShort" = "is too short (must be at least 8 characters)"; +"Common.Errors.ErrUnreachable" = "does not seem to exist"; "Common.Errors.Item.Agreement" = "agreement"; "Common.Errors.Item.Email" = "email"; "Common.Errors.Item.Locale" = "locale"; "Common.Errors.Item.Password" = "password"; "Common.Errors.Item.Reason" = "reason"; "Common.Errors.Item.Username" = "username"; +"Common.Errors.Itemdetail.Emailinvalid" = "It's not a valid e-mail address"; +"Common.Errors.Itemdetail.Usernameinvalid" = "username only contains alphanumeric characters and underscores"; "Scene.ConfirmEmail.Button.DontReceiveEmail" = "I never got an email"; "Scene.ConfirmEmail.Button.OpenEmailApp" = "Open Email App"; "Scene.ConfirmEmail.DontReceiveEmail.Description" = "Check if your email address is correct as well as your junk folder if you haven’t."; diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift index 8f930771a..a32e5d040 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewModel.swift @@ -160,7 +160,7 @@ extension MastodonRegisterViewModel { let falseColor = UIColor.clear let attributeString = NSMutableAttributedString() - let start = NSAttributedString(string: "Your password needs at least:\n", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) + let start = NSAttributedString(string: "Your password needs at least:", attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: color]) attributeString.append(start) attributeString.append(checkmarkImage(color: eightCharacters ? color : falseColor)) diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ErrorDetail.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ErrorDetail.swift index 4d90779bb..7881aaa0d 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ErrorDetail.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+ErrorDetail.swift @@ -7,15 +7,15 @@ import Foundation extension Mastodon.Entity.Error { - /// ERR_BLOCKED When e-mail provider is not allowed - /// ERR_UNREACHABLE When e-mail address does not resolve to any IP via DNS (MX, A, AAAA) - /// ERR_TAKEN When username or e-mail are already taken - /// ERR_RESERVED When a username is reserved, e.g. "webmaster" or "admin" - /// ERR_ACCEPTED When agreement has not been accepted - /// ERR_BLANK When a required attribute is blank - /// ERR_INVALID When an attribute is malformed, e.g. wrong characters or invalid e-mail address - /// ERR_TOO_LONG When an attribute is over the character limit - /// ERR_INCLUSION When an attribute is not one of the allowed values, e.g. unsupported locale + /// ERR_BLOCKED When e-mail provider is not allowed + /// ERR_UNREACHABLE When e-mail address does not resolve to any IP via DNS (MX, A, AAAA) + /// ERR_TAKEN When username or e-mail are already taken + /// ERR_RESERVED When a username is reserved, e.g. "webmaster" or "admin" + /// ERR_ACCEPTED When agreement has not been accepted + /// ERR_BLANK When a required attribute is blank + /// ERR_INVALID When an attribute is malformed, e.g. wrong characters or invalid e-mail address + /// ERR_TOO_LONG When an attribute is over the character limit + /// ERR_INCLUSION When an attribute is not one of the allowed values, e.g. unsupported locale public enum SignUpError: RawRepresentable, Codable { case ERR_BLOCKED case ERR_UNREACHABLE From f6d9b127224346111320660a0227ed7f0a5e4285 Mon Sep 17 00:00:00 2001 From: sunxiaojian Date: Tue, 2 Mar 2021 10:25:49 +0800 Subject: [PATCH 6/6] chore: update the i18n suggests --- Localization/app.json | 12 ++++++---- Mastodon.xcodeproj/project.pbxproj | 12 ++++++---- ...> Mastodon+Entidy+ErrorDetailReason.swift} | 23 ++++++++++++------- Mastodon/Extension/String.swift | 18 +++++++++++++++ Mastodon/Generated/Strings.swift | 18 +++++++++------ .../Resources/en.lproj/Localizable.strings | 12 ++++++---- 6 files changed, 66 insertions(+), 29 deletions(-) rename Mastodon/Extension/{Mastodon+Entidy+ErrorDetailReson.swift => Mastodon+Entidy+ErrorDetailReason.swift} (80%) create mode 100644 Mastodon/Extension/String.swift diff --git a/Localization/app.json b/Localization/app.json index b5d375a15..3a45922a5 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -10,18 +10,20 @@ "reason": "reason" }, "itemDetail": { - "emailInvalid": "It's not a valid e-mail address", - "usernameInvalid": "username only contains alphanumeric characters and underscores" + "email_invalid": "This is not a valid e-mail address", + "username_invalid": "Username must only contain alphanumeric characters and underscores", + "password_too_shrot": "password is too short (must be at least 8 characters)", + "username_too_long": "username is too long (can't be longer than 30 characters)" }, "ERR_BLOCKED": "contains a disallowed e-mail provider", "ERR_UNREACHABLE": "does not seem to exist", "ERR_TAKEN": "is already in use", - "ERR_RESERVED": "is a reserved keyword or username", + "ERR_RESERVED": "is a reserved keyword", "ERR_ACCEPTED": "must be accepted", "ERR_BLANK": "is required", "ERR_INVALID": "is invalid", - "ERR_TOO_LONG": "is too long ( can't be longer than 30 characters)", - "ERR_TOO_SHORT": "is too short (must be at least 8 characters)", + "ERR_TOO_LONG": "is too long", + "ERR_TOO_SHORT": "is too short", "ERR_INCLUSION": "is not a supported value" }, "alerts": { diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 4b93b1f99..9b5894130 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -55,7 +55,7 @@ 2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */; }; 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; }; - 2D650FAB25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReson.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReson.swift */; }; + 2D650FAB25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReason.swift */; }; 2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */; }; 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */; }; 2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */; }; @@ -71,6 +71,7 @@ 2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0725C7E9A8004F19B8 /* Tag.swift */; }; 2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0D25C7E9C9004F19B8 /* History.swift */; }; 2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F1325C7EDD9004F19B8 /* Emoji.swift */; }; + 2D939AB525EDD8A90076FA61 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AB425EDD8A90076FA61 /* String.swift */; }; 2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */; }; 2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; }; 2DA7D05725CA693F00804E11 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D05625CA693F00804E11 /* Application.swift */; }; @@ -261,7 +262,7 @@ 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewController+DebugAction.swift"; sourceTree = ""; }; 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+Timeline.swift"; sourceTree = ""; }; 2D61335D25C1894B00CAE157 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; - 2D650FAA25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReson.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entidy+ErrorDetailReson.swift"; sourceTree = ""; }; + 2D650FAA25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReason.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entidy+ErrorDetailReason.swift"; sourceTree = ""; }; 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = ""; }; 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Toot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Toot.swift"; sourceTree = ""; }; 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewController.swift; sourceTree = ""; }; @@ -277,6 +278,7 @@ 2D927F0725C7E9A8004F19B8 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = ""; }; 2D927F0D25C7E9C9004F19B8 /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = ""; }; 2D927F1325C7EDD9004F19B8 /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = ""; }; + 2D939AB425EDD8A90076FA61 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLoaderTableViewCell.swift; sourceTree = ""; }; 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderTableViewCell.swift; sourceTree = ""; }; 2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = ""; }; @@ -1009,7 +1011,8 @@ DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */, 2D32EAB925CB9B0500C9ED86 /* UIView.swift */, 0FAA101B25E10E760017CCDE /* UIFont.swift */, - 2D650FAA25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReson.swift */, + 2D650FAA25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReason.swift */, + 2D939AB425EDD8A90076FA61 /* String.swift */, ); path = Extension; sourceTree = ""; @@ -1460,6 +1463,7 @@ DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */, DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */, 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */, + 2D939AB525EDD8A90076FA61 /* String.swift in Sources */, 0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */, 2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */, DB98338825C945ED00AD9700 /* Assets.swift in Sources */, @@ -1505,7 +1509,7 @@ 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */, DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */, 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, - 2D650FAB25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReson.swift in Sources */, + 2D650FAB25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReason.swift in Sources */, DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */, DB084B5725CBC56C00F898ED /* Toot.swift in Sources */, DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */, diff --git a/Mastodon/Extension/Mastodon+Entidy+ErrorDetailReson.swift b/Mastodon/Extension/Mastodon+Entidy+ErrorDetailReason.swift similarity index 80% rename from Mastodon/Extension/Mastodon+Entidy+ErrorDetailReson.swift rename to Mastodon/Extension/Mastodon+Entidy+ErrorDetailReason.swift index e72e2771f..cc1a47907 100644 --- a/Mastodon/Extension/Mastodon+Entidy+ErrorDetailReson.swift +++ b/Mastodon/Extension/Mastodon+Entidy+ErrorDetailReason.swift @@ -1,5 +1,5 @@ // -// Mastodon+Entidy+ErrorDetailReason.swift +// Mastodon+Entity+ErrorDetailReason.swift // Mastodon // // Created by sxiaojian on 2021/3/1. @@ -41,9 +41,12 @@ extension Mastodon.Entity.ErrorDetail { if let username = self.username, !username.isEmpty { let errors = username.map { errorDetailReason -> String in - if errorDetailReason.error == .ERR_INVALID { - return L10n.Common.Errors.Itemdetail.usernameinvalid - } else { + switch errorDetailReason.error { + case .ERR_INVALID: + return L10n.Common.Errors.Itemdetail.usernameInvalid + case .ERR_TOO_LONG: + return L10n.Common.Errors.Itemdetail.usernameTooLong + default: return L10n.Common.Errors.Item.username + " " + errorDetailReason.localizedDescription() } } @@ -53,7 +56,7 @@ extension Mastodon.Entity.ErrorDetail { if let email = self.email, !email.isEmpty { let errors = email.map { errorDetailReason -> String in if errorDetailReason.error == .ERR_INVALID { - return L10n.Common.Errors.Itemdetail.emailinvalid + return L10n.Common.Errors.Itemdetail.emailInvalid } else { return L10n.Common.Errors.Item.email + " " + errorDetailReason.localizedDescription() } @@ -61,8 +64,12 @@ extension Mastodon.Entity.ErrorDetail { messages.append(contentsOf: errors) } if let password = self.password,!password.isEmpty { - let errors = password.map { - L10n.Common.Errors.Item.password + " " + $0.localizedDescription() + let errors = password.map { errorDetailReason -> String in + if errorDetailReason.error == .ERR_TOO_SHORT { + return L10n.Common.Errors.Itemdetail.passwordTooShrot + } else { + return L10n.Common.Errors.Item.password + " " + errorDetailReason.localizedDescription() + } } messages.append(contentsOf: errors) } @@ -87,6 +94,6 @@ extension Mastodon.Entity.ErrorDetail { let message = messages .compactMap { $0 } .joined(separator: ", ") - return message + return message.capitalizingFirstLetter() } } diff --git a/Mastodon/Extension/String.swift b/Mastodon/Extension/String.swift new file mode 100644 index 000000000..87028ffdf --- /dev/null +++ b/Mastodon/Extension/String.swift @@ -0,0 +1,18 @@ +// +// String.swift +// Mastodon +// +// Created by sxiaojian on 2021/3/2. +// + +import Foundation + +extension String { + func capitalizingFirstLetter() -> String { + return prefix(1).capitalized + dropFirst() + } + + mutating func capitalizeFirstLetter() { + self = self.capitalizingFirstLetter() + } +} diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index d9e7de9e5..8e93c804e 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -93,13 +93,13 @@ internal enum L10n { internal static let errInclusion = L10n.tr("Localizable", "Common.Errors.ErrInclusion") /// is invalid internal static let errInvalid = L10n.tr("Localizable", "Common.Errors.ErrInvalid") - /// is a reserved keyword or username + /// is a reserved keyword internal static let errReserved = L10n.tr("Localizable", "Common.Errors.ErrReserved") /// is already in use internal static let errTaken = L10n.tr("Localizable", "Common.Errors.ErrTaken") - /// is too long ( can't be longer than 30 characters) + /// is too long internal static let errTooLong = L10n.tr("Localizable", "Common.Errors.ErrTooLong") - /// is too short (must be at least 8 characters) + /// is too short internal static let errTooShort = L10n.tr("Localizable", "Common.Errors.ErrTooShort") /// does not seem to exist internal static let errUnreachable = L10n.tr("Localizable", "Common.Errors.ErrUnreachable") @@ -118,10 +118,14 @@ internal enum L10n { internal static let username = L10n.tr("Localizable", "Common.Errors.Item.Username") } internal enum Itemdetail { - /// It's not a valid e-mail address - internal static let emailinvalid = L10n.tr("Localizable", "Common.Errors.Itemdetail.Emailinvalid") - /// username only contains alphanumeric characters and underscores - internal static let usernameinvalid = L10n.tr("Localizable", "Common.Errors.Itemdetail.Usernameinvalid") + /// This is not a valid e-mail address + internal static let emailInvalid = L10n.tr("Localizable", "Common.Errors.Itemdetail.EmailInvalid") + /// password is too short (must be at least 8 characters) + internal static let passwordTooShrot = L10n.tr("Localizable", "Common.Errors.Itemdetail.PasswordTooShrot") + /// Username must only contain alphanumeric characters and underscores + internal static let usernameInvalid = L10n.tr("Localizable", "Common.Errors.Itemdetail.UsernameInvalid") + /// username is too long ( can't be longer than 30 characters) + internal static let usernameTooLong = L10n.tr("Localizable", "Common.Errors.Itemdetail.UsernameTooLong") } } } diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index d127a93a3..191ec0daf 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -28,10 +28,10 @@ "Common.Errors.ErrBlocked" = "contains a disallowed e-mail provider"; "Common.Errors.ErrInclusion" = "is not a supported value"; "Common.Errors.ErrInvalid" = "is invalid"; -"Common.Errors.ErrReserved" = "is a reserved keyword or username"; +"Common.Errors.ErrReserved" = "is a reserved keyword"; "Common.Errors.ErrTaken" = "is already in use"; -"Common.Errors.ErrTooLong" = "is too long ( can't be longer than 30 characters)"; -"Common.Errors.ErrTooShort" = "is too short (must be at least 8 characters)"; +"Common.Errors.ErrTooLong" = "is too long"; +"Common.Errors.ErrTooShort" = "is too short"; "Common.Errors.ErrUnreachable" = "does not seem to exist"; "Common.Errors.Item.Agreement" = "agreement"; "Common.Errors.Item.Email" = "email"; @@ -39,8 +39,10 @@ "Common.Errors.Item.Password" = "password"; "Common.Errors.Item.Reason" = "reason"; "Common.Errors.Item.Username" = "username"; -"Common.Errors.Itemdetail.Emailinvalid" = "It's not a valid e-mail address"; -"Common.Errors.Itemdetail.Usernameinvalid" = "username only contains alphanumeric characters and underscores"; +"Common.Errors.Itemdetail.EmailInvalid" = "This is not a valid e-mail address"; +"Common.Errors.Itemdetail.PasswordTooShrot" = "password is too short (must be at least 8 characters)"; +"Common.Errors.Itemdetail.UsernameInvalid" = "Username must only contain alphanumeric characters and underscores"; +"Common.Errors.Itemdetail.UsernameTooLong" = "username is too long ( can't be longer than 30 characters)"; "Scene.ConfirmEmail.Button.DontReceiveEmail" = "I never got an email"; "Scene.ConfirmEmail.Button.OpenEmailApp" = "Open Email App"; "Scene.ConfirmEmail.DontReceiveEmail.Description" = "Check if your email address is correct as well as your junk folder if you haven’t.";