mirror of
https://github.com/mastodon/mastodon-ios
synced 2025-04-11 22:58:02 +02:00
Add age verification to new account registration on servers that require it
Also improve the form fill experience by only marking a field invalid after it has been edited at least once, or was valid and then became invalid. (Or, for date of birth, is the last thing standing in the way of everything being valid, since the birth date is pre-filled with a likely-invalid value.) Contributes to IOS-378
This commit is contained in:
parent
9f1291ca36
commit
597d704d4e
@ -14,7 +14,7 @@ final class PrivacyViewModel {
|
||||
let domain: String
|
||||
let authenticateInfo: AuthenticationViewModel.AuthenticateInfo
|
||||
let rows: [PrivacyRow]
|
||||
let instance: Mastodon.Entity.Instance
|
||||
let instance: RegistrationInstance
|
||||
let applicationToken: Mastodon.Entity.Token
|
||||
let didAccept: ()->()
|
||||
|
||||
@ -22,7 +22,7 @@ final class PrivacyViewModel {
|
||||
domain: String,
|
||||
authenticateInfo: AuthenticationViewModel.AuthenticateInfo,
|
||||
rows: [PrivacyRow],
|
||||
instance: Mastodon.Entity.Instance,
|
||||
instance: RegistrationInstance,
|
||||
applicationToken: Mastodon.Entity.Token,
|
||||
didAccept: @escaping ()->()
|
||||
) {
|
||||
|
@ -13,19 +13,27 @@ import MastodonAsset
|
||||
|
||||
struct MastodonRegisterView: View {
|
||||
|
||||
@FocusState var focusedField: MastodonRegisterViewModel.RegistrationField?
|
||||
|
||||
@ObservedObject var viewModel: MastodonRegisterViewModel
|
||||
|
||||
@State var usernameRightViewWidth: CGFloat = 300
|
||||
|
||||
@State var dateOfBirthLabel = L10n.Scene.Register.Input.BirthDate.label.localizedCapitalized
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.vertical) {
|
||||
let margin: CGFloat = 16
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Spacer()
|
||||
if let minAge = viewModel.minAge {
|
||||
dateOfBirthEntry(minAge: minAge)
|
||||
}
|
||||
TextField(L10n.Scene.Register.Input.DisplayName.placeholder.localizedCapitalized, text: $viewModel.name)
|
||||
.textContentType(.name)
|
||||
.disableAutocorrection(true)
|
||||
.modifier(FormTextFieldModifier(validateState: viewModel.displayNameValidateState))
|
||||
.focused($focusedField, equals: .displayName)
|
||||
HStack {
|
||||
Text("@")
|
||||
.accessibilityHidden(true)
|
||||
@ -35,6 +43,7 @@ struct MastodonRegisterView: View {
|
||||
.disableAutocorrection(true)
|
||||
.keyboardType(.asciiCapable)
|
||||
.accessibilityLabel(viewModel.accessibilityLabelUsernameField)
|
||||
.focused($focusedField, equals: .handle)
|
||||
Text("@\(viewModel.domain)")
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
@ -69,6 +78,7 @@ struct MastodonRegisterView: View {
|
||||
.disableAutocorrection(true)
|
||||
.keyboardType(.emailAddress)
|
||||
.modifier(FormTextFieldModifier(validateState: viewModel.emailValidateState))
|
||||
.focused($focusedField, equals: .email)
|
||||
if let errorPrompt = viewModel.emailErrorPrompt {
|
||||
Text(errorPrompt)
|
||||
.modifier(FormFootnoteModifier())
|
||||
@ -82,10 +92,12 @@ struct MastodonRegisterView: View {
|
||||
VStack(alignment: .leading, spacing: margin) {
|
||||
SecureField(L10n.Scene.Register.Input.Password.placeholder.localizedCapitalized, text: $viewModel.password)
|
||||
.textContentType(.newPassword)
|
||||
.modifier(FormTextFieldModifier(validateState: viewModel.passwordValidateState))
|
||||
.modifier(FormTextFieldModifier(validateState: viewModel.passwordBaseValidateState))
|
||||
.focused($focusedField, equals: .password)
|
||||
SecureField(L10n.Scene.Register.Input.Password.confirmationPlaceholder.localizedCapitalized, text: $viewModel.passwordConfirmation)
|
||||
.textContentType(.newPassword)
|
||||
.modifier(FormTextFieldModifier(validateState: viewModel.passwordValidateState))
|
||||
.modifier(FormTextFieldModifier(validateState: viewModel.passwordConfirmationValidateState))
|
||||
.focused($focusedField, equals: .confirmPassword)
|
||||
Text(L10n.Scene.Register.Input.Password.hint)
|
||||
.modifier(FormFootnoteModifier(foregroundColor: .secondary))
|
||||
if let errorPrompt = viewModel.passwordErrorPrompt {
|
||||
@ -101,6 +113,7 @@ struct MastodonRegisterView: View {
|
||||
VStack(alignment: .leading, spacing: 11) {
|
||||
TextField(L10n.Scene.Register.Input.Invite.registrationUserInviteRequest.localizedCapitalized, text: $viewModel.reason)
|
||||
.modifier(FormTextFieldModifier(validateState: viewModel.reasonValidateState))
|
||||
.focused($focusedField, equals: .proposedApprovalReason)
|
||||
if let errorPrompt = viewModel.reasonErrorPrompt {
|
||||
Text(errorPrompt)
|
||||
.modifier(FormFootnoteModifier())
|
||||
@ -119,6 +132,9 @@ struct MastodonRegisterView: View {
|
||||
}
|
||||
)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.onChange(of: focusedField) { _, newValue in
|
||||
viewModel.editingField = newValue
|
||||
}
|
||||
}
|
||||
|
||||
struct FormTextFieldModifier: ViewModifier {
|
||||
@ -128,7 +144,7 @@ struct MastodonRegisterView: View {
|
||||
ZStack {
|
||||
let borderColor: Color = {
|
||||
switch validateState {
|
||||
case .empty: return Color(Asset.Scene.Onboarding.textFieldBackground.color)
|
||||
case .empty, .filling: return Color(Asset.Scene.Onboarding.textFieldBackground.color)
|
||||
case .invalid: return Color(Asset.Colors.TextField.invalid.color.withAlphaComponent(0.25))
|
||||
case .valid: return Color(Asset.Scene.Onboarding.textFieldBackground.color)
|
||||
}
|
||||
@ -153,7 +169,22 @@ struct MastodonRegisterView: View {
|
||||
.foregroundColor(foregroundColor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder func dateOfBirthEntry(minAge: Int) -> some View {
|
||||
VStack {
|
||||
ZStack {
|
||||
TextField(L10n.Scene.Register.Input.BirthDate.label.localizedCapitalized, text: $dateOfBirthLabel)
|
||||
.disabled(true)
|
||||
.modifier(FormTextFieldModifier(validateState: viewModel.dateOfBirthValidateState))
|
||||
HStack {
|
||||
Spacer().frame(maxWidth: .infinity)
|
||||
DatePicker(selection: $viewModel.dateOfBirth, in: ...Date.now, displayedComponents: .date) { }
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
Text(L10n.Scene.Register.Input.BirthDate.explanationMessage(minAge, viewModel.domain)).font(.callout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WidthKey: PreferenceKey {
|
||||
|
@ -12,20 +12,34 @@ import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonCore
|
||||
import MastodonLocalization
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
final class MastodonRegisterViewModel: ObservableObject {
|
||||
|
||||
enum RegistrationField: Hashable {
|
||||
case displayName
|
||||
case handle
|
||||
case email
|
||||
case password
|
||||
case confirmPassword
|
||||
case dateOfBirth
|
||||
case proposedApprovalReason
|
||||
}
|
||||
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// input
|
||||
let domain: String
|
||||
let authenticateInfo: AuthenticationViewModel.AuthenticateInfo
|
||||
let instance: Mastodon.Entity.Instance
|
||||
let instance: RegistrationInstance
|
||||
let applicationToken: Mastodon.Entity.Token
|
||||
let viewDidAppear = CurrentValueSubject<Void, Never>(Void())
|
||||
let submitValidatedUserRegistration: (MastodonRegisterViewModel, Bool) async -> ()
|
||||
|
||||
@Published var backgroundColor: UIColor = Asset.Scene.Onboarding.background.color
|
||||
@Published var dateOfBirth = Date.now
|
||||
@Published var name = ""
|
||||
@Published var username = ""
|
||||
@Published var email = ""
|
||||
@ -43,13 +57,24 @@ final class MastodonRegisterViewModel: ObservableObject {
|
||||
// output
|
||||
var diffableDataSource: UITableViewDiffableDataSource<RegisterSection, RegisterItem>?
|
||||
let approvalRequired: Bool
|
||||
let minAge: Int?
|
||||
let applicationAuthorization: Mastodon.API.OAuth.Authorization
|
||||
|
||||
@Published var dateOfBirthValidateState: ValidateState = .empty
|
||||
@Published var usernameValidateState: ValidateState = .empty
|
||||
@Published var displayNameValidateState: ValidateState = .empty
|
||||
@Published var emailValidateState: ValidateState = .empty
|
||||
@Published var passwordValidateState: ValidateState = .empty
|
||||
@Published var passwordBaseValidateState: ValidateState = .empty
|
||||
@Published var passwordConfirmationValidateState: ValidateState = .empty
|
||||
@Published var reasonValidateState: ValidateState = .empty
|
||||
|
||||
public var editingField: RegistrationField? {
|
||||
didSet {
|
||||
if let oldValue {
|
||||
validate(oldValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published var isRegistering = false
|
||||
@Published var isAllValid = false
|
||||
@ -60,7 +85,7 @@ final class MastodonRegisterViewModel: ObservableObject {
|
||||
init(
|
||||
domain: String,
|
||||
authenticateInfo: AuthenticationViewModel.AuthenticateInfo,
|
||||
instance: Mastodon.Entity.Instance,
|
||||
instance: RegistrationInstance,
|
||||
applicationToken: Mastodon.Entity.Token,
|
||||
submitValidatedUserRegistration: @escaping (MastodonRegisterViewModel, Bool) async ->()
|
||||
) {
|
||||
@ -69,36 +94,66 @@ final class MastodonRegisterViewModel: ObservableObject {
|
||||
self.instance = instance
|
||||
self.applicationToken = applicationToken
|
||||
self.approvalRequired = instance.approvalRequired ?? false
|
||||
self.minAge = instance.minAge
|
||||
self.applicationAuthorization = Mastodon.API.OAuth.Authorization(accessToken: applicationToken.accessToken)
|
||||
self.submitValidatedUserRegistration = submitValidatedUserRegistration
|
||||
|
||||
$dateOfBirth
|
||||
.map { [weak self] dob in
|
||||
guard let self else { return .invalid }
|
||||
switch dateOfBirthValidateState {
|
||||
case .empty:
|
||||
return .filling
|
||||
case .filling:
|
||||
if self.validate(dateOfBirth: dob) == .valid {
|
||||
return .valid
|
||||
} else {
|
||||
return .filling
|
||||
}
|
||||
case .invalid, .valid:
|
||||
return self.validate(dateOfBirth: dob)
|
||||
}
|
||||
}
|
||||
.assign(to: \.dateOfBirthValidateState, on: self)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
$name
|
||||
.map { name in
|
||||
.map { [weak self] name in
|
||||
guard !name.isEmpty else { return .empty }
|
||||
return .valid
|
||||
guard let self else { return .invalid }
|
||||
switch self.displayNameValidateState {
|
||||
case .empty:
|
||||
return .filling
|
||||
case .filling:
|
||||
if self.validate(displayName: name) == .valid {
|
||||
return .valid
|
||||
} else {
|
||||
return .filling
|
||||
}
|
||||
case .invalid, .valid:
|
||||
return self.validate(displayName: name)
|
||||
}
|
||||
}
|
||||
.assign(to: \.displayNameValidateState, on: self)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
$username
|
||||
.removeDuplicates()
|
||||
.map { username in
|
||||
.map { [weak self] username in
|
||||
guard !username.isEmpty else { return .empty }
|
||||
var isValid = true
|
||||
|
||||
// regex opt-out way to check validation
|
||||
// allowed:
|
||||
// a-z (isASCII && isLetter)
|
||||
// A-Z (isASCII && isLetter)
|
||||
// 0-9 (isASCII && isNumber)
|
||||
// _ ("_")
|
||||
for char in username {
|
||||
guard char.isASCII, char.isLetter || char.isNumber || char == "_" else {
|
||||
isValid = false
|
||||
break
|
||||
guard let self else { return .invalid }
|
||||
switch self.usernameValidateState {
|
||||
case .empty:
|
||||
return .filling
|
||||
case .filling:
|
||||
if self.validate(handle: username) == .valid {
|
||||
return .valid
|
||||
} else {
|
||||
return .filling
|
||||
}
|
||||
case .invalid, .valid:
|
||||
return self.validate(handle: username)
|
||||
}
|
||||
return isValid ? .valid : .invalid
|
||||
}
|
||||
.assign(to: \.usernameValidateState, on: self)
|
||||
.store(in: &disposeBag)
|
||||
@ -145,31 +200,79 @@ final class MastodonRegisterViewModel: ObservableObject {
|
||||
.store(in: &disposeBag)
|
||||
|
||||
$email
|
||||
.map { email in
|
||||
.map { [weak self] email in
|
||||
guard !email.isEmpty else { return .empty }
|
||||
return MastodonRegisterViewModel.isValidEmail(email) ? .valid : .invalid
|
||||
guard let self else { return .invalid }
|
||||
switch self.emailValidateState {
|
||||
case .empty:
|
||||
return .filling
|
||||
case .filling:
|
||||
if self.validate(email: email) == .valid {
|
||||
return .valid
|
||||
} else {
|
||||
return .filling
|
||||
}
|
||||
case .invalid, .valid:
|
||||
return self.validate(email: email)
|
||||
}
|
||||
}
|
||||
.assign(to: \.emailValidateState, on: self)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest($password, $passwordConfirmation)
|
||||
.map { password, confirmation in
|
||||
guard !password.isEmpty && !confirmation.isEmpty else { return .empty }
|
||||
|
||||
if password.count >= 8 && password == confirmation {
|
||||
return .valid
|
||||
} else {
|
||||
return .invalid
|
||||
$password
|
||||
.map { [weak self] password in
|
||||
guard !password.isEmpty else { return .empty }
|
||||
guard let self else { return .invalid }
|
||||
switch self.passwordBaseValidateState {
|
||||
case .empty:
|
||||
return .filling
|
||||
case .filling:
|
||||
if self.validate(password: password) == .valid {
|
||||
return .valid
|
||||
} else {
|
||||
return .filling
|
||||
}
|
||||
case .invalid, .valid:
|
||||
return self.validate(password: password)
|
||||
}
|
||||
}
|
||||
.assign(to: \.passwordValidateState, on: self)
|
||||
.assign(to: \.passwordBaseValidateState, on: self)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest($password, $passwordConfirmation)
|
||||
.map { [weak self] password, confirmation in
|
||||
guard !password.isEmpty && !confirmation.isEmpty else { return .empty }
|
||||
guard let self else { return .invalid }
|
||||
switch self.passwordConfirmationValidateState {
|
||||
case .empty, .filling:
|
||||
if self.validate(password: password, confirmation: confirmation) == .valid {
|
||||
return .valid
|
||||
} else {
|
||||
return .filling
|
||||
}
|
||||
case .invalid, .valid:
|
||||
return self.validate(password: password, confirmation: confirmation)
|
||||
}
|
||||
}
|
||||
.assign(to: \.passwordConfirmationValidateState, on: self)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
if approvalRequired {
|
||||
$reason
|
||||
.map { invite in
|
||||
guard !invite.isEmpty else { return .empty }
|
||||
return .valid
|
||||
.map { joinReason in
|
||||
guard !joinReason.isEmpty else { return .empty }
|
||||
switch self.reasonValidateState {
|
||||
case .empty:
|
||||
return .filling
|
||||
case .filling:
|
||||
if self.validate(reason: joinReason) == .valid {
|
||||
return .valid
|
||||
} else {
|
||||
return .filling
|
||||
}
|
||||
case .invalid, .valid:
|
||||
return self.validate(reason: joinReason)
|
||||
}
|
||||
}
|
||||
.assign(to: \.reasonValidateState, on: self)
|
||||
.store(in: &disposeBag)
|
||||
@ -188,7 +291,7 @@ final class MastodonRegisterViewModel: ObservableObject {
|
||||
self.emailErrorPrompt = details.emailErrorDescriptions.first
|
||||
details.emailErrorDescriptions.first.flatMap { _ in self.emailValidateState = .invalid }
|
||||
self.passwordErrorPrompt = details.passwordErrorDescriptions.first
|
||||
details.passwordErrorDescriptions.first.flatMap { _ in self.passwordValidateState = .invalid }
|
||||
details.passwordErrorDescriptions.first.flatMap { _ in self.passwordBaseValidateState = .invalid }
|
||||
self.reasonErrorPrompt = details.reasonErrorDescriptions.first
|
||||
details.reasonErrorDescriptions.first.flatMap { _ in self.reasonValidateState = .invalid }
|
||||
} else {
|
||||
@ -204,7 +307,7 @@ final class MastodonRegisterViewModel: ObservableObject {
|
||||
$usernameValidateState,
|
||||
$displayNameValidateState,
|
||||
$emailValidateState,
|
||||
$passwordValidateState
|
||||
$passwordBaseValidateState
|
||||
)
|
||||
.map {
|
||||
$0.0 == .valid &&
|
||||
@ -213,9 +316,17 @@ final class MastodonRegisterViewModel: ObservableObject {
|
||||
$0.3 == .valid
|
||||
}
|
||||
|
||||
let publisherTwo = $reasonValidateState.map { reasonValidateState -> Bool in
|
||||
guard self.approvalRequired else { return true }
|
||||
return reasonValidateState == .valid
|
||||
let publisherTwo = Publishers.CombineLatest3(
|
||||
$reasonValidateState,
|
||||
$dateOfBirthValidateState,
|
||||
$passwordConfirmationValidateState
|
||||
)
|
||||
.map { [weak self] reasonValidateState, dobValidateState, passwordConfirmationValidateState -> Bool in
|
||||
guard let self else { return false }
|
||||
let reasonOK = !self.approvalRequired || reasonValidateState == .valid
|
||||
let dobOK = (self.minAge == nil) || dobValidateState == .valid
|
||||
let passwordConfirmationCorrect = passwordConfirmationValidateState == .valid
|
||||
return reasonOK && dobOK && passwordConfirmationCorrect
|
||||
}
|
||||
|
||||
Publishers.CombineLatest(
|
||||
@ -225,25 +336,123 @@ final class MastodonRegisterViewModel: ObservableObject {
|
||||
.map { $0 && $1 }
|
||||
.assign(to: \.isAllValid, on: self)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest4(
|
||||
publisherOne,
|
||||
$reasonValidateState,
|
||||
$passwordConfirmationValidateState,
|
||||
$dateOfBirthValidateState
|
||||
)
|
||||
.sink { [weak self] publisherOne, reasonValidState, passwordConfirmValidState, dobValidState in
|
||||
if publisherOne == false { return }
|
||||
if reasonValidState == .valid && passwordConfirmValidState == .valid && dobValidState != .valid {
|
||||
self?.dateOfBirthValidateState = .invalid // this will highlight the DOB field if everything else has been filled in
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonRegisterViewModel {
|
||||
enum ValidateState: Hashable {
|
||||
case empty
|
||||
case filling
|
||||
case invalid
|
||||
case valid
|
||||
}
|
||||
|
||||
static func isValidEmail(_ email: String) -> Bool {
|
||||
let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
|
||||
|
||||
let emailPred = NSPredicate(format: "SELF MATCHES %@", emailRegEx)
|
||||
return emailPred.evaluate(with: email)
|
||||
}
|
||||
|
||||
func validate(_ field: RegistrationField) {
|
||||
let state = validationState(forCurrentContentsOf: field)
|
||||
switch field {
|
||||
case .displayName:
|
||||
displayNameValidateState = state
|
||||
case .handle:
|
||||
usernameValidateState = state
|
||||
case .email:
|
||||
emailValidateState = state
|
||||
case .password:
|
||||
passwordBaseValidateState = state
|
||||
case .confirmPassword:
|
||||
passwordConfirmationValidateState = state
|
||||
case .dateOfBirth:
|
||||
dateOfBirthValidateState = state
|
||||
case .proposedApprovalReason:
|
||||
reasonValidateState = state
|
||||
}
|
||||
}
|
||||
|
||||
private func validate(dateOfBirth: Date) -> ValidateState {
|
||||
guard let minAge else { return .valid }
|
||||
let years = Calendar.current.dateComponents([.year], from: dateOfBirth, to: Date.now).year ?? 0
|
||||
print("looks to be \(years) old")
|
||||
return years < minAge ? .invalid : .valid
|
||||
}
|
||||
|
||||
private func validate(displayName: String) -> ValidateState {
|
||||
return displayName.isEmpty ? .empty : .valid
|
||||
}
|
||||
|
||||
private func validate(handle: String) -> ValidateState {
|
||||
var isValid = true
|
||||
// regex opt-out way to check validation
|
||||
// allowed:
|
||||
// a-z (isASCII && isLetter)
|
||||
// A-Z (isASCII && isLetter)
|
||||
// 0-9 (isASCII && isNumber)
|
||||
// _ ("_")
|
||||
for char in handle {
|
||||
guard char.isASCII, char.isLetter || char.isNumber || char == "_" else {
|
||||
isValid = false
|
||||
break
|
||||
}
|
||||
}
|
||||
return isValid ? .valid : .invalid
|
||||
}
|
||||
|
||||
private func validate(email: String) -> ValidateState {
|
||||
return MastodonRegisterViewModel.isValidEmail(email) ? .valid : .invalid
|
||||
}
|
||||
|
||||
private func validate(password: String) -> ValidateState {
|
||||
return password.count >= 8 ? .valid : .invalid
|
||||
}
|
||||
|
||||
private func validate(password: String, confirmation: String) -> ValidateState {
|
||||
return password == passwordConfirmation ? .valid : .invalid
|
||||
}
|
||||
|
||||
private func validate(reason: String) -> ValidateState {
|
||||
return reason.isEmpty ? .invalid : .valid
|
||||
}
|
||||
|
||||
private func validationState(forCurrentContentsOf field: RegistrationField) -> ValidateState {
|
||||
switch field {
|
||||
case .displayName:
|
||||
return validate(displayName: name)
|
||||
case .handle:
|
||||
return validate(handle: username)
|
||||
case .email:
|
||||
return validate(email: email)
|
||||
case .password:
|
||||
return validate(password: password)
|
||||
case .confirmPassword:
|
||||
return validate(password: password, confirmation: passwordConfirmation)
|
||||
case .dateOfBirth:
|
||||
return validate(dateOfBirth: dateOfBirth)
|
||||
case .proposedApprovalReason:
|
||||
return validate(reason: reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonRegisterViewModel {
|
||||
static func isValidEmail(_ email: String) -> Bool {
|
||||
let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
|
||||
|
||||
let emailPred = NSPredicate(format: "SELF MATCHES %@", emailRegEx)
|
||||
return emailPred.evaluate(with: email)
|
||||
}
|
||||
|
||||
static func checkmarkImage(font: UIFont = .preferredFont(forTextStyle: .caption1)) -> UIImage {
|
||||
let configuration = UIImage.SymbolConfiguration(font: font)
|
||||
return UIImage(systemName: "checkmark.circle.fill", withConfiguration: configuration)!
|
||||
@ -294,3 +503,25 @@ extension MastodonRegisterViewModel {
|
||||
return "@\(username)@\(domain)"
|
||||
}
|
||||
}
|
||||
|
||||
protocol RegistrationInstance {
|
||||
var approvalRequired: Bool? { get }
|
||||
var minAge: Int? { get }
|
||||
var isBeyondVersion1: Bool { get }
|
||||
var isOpenToNewRegistrations: Bool? { get }
|
||||
var rules: [Mastodon.Entity.Instance.Rule]? { get }
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Instance: RegistrationInstance {
|
||||
var minAge: Int? { return nil }
|
||||
var isBeyondVersion1: Bool {
|
||||
return version?.majorServerVersion(greaterThanOrEquals: 4) ?? false
|
||||
}
|
||||
var isOpenToNewRegistrations: Bool? { return registrations }
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.V2.Instance: RegistrationInstance {
|
||||
var minAge: Int? { return registrations?.minAge }
|
||||
var isBeyondVersion1: Bool { return true }
|
||||
var isOpenToNewRegistrations: Bool? { return registrations?.enabled }
|
||||
}
|
||||
|
@ -103,9 +103,17 @@ extension AuthenticationViewModel {
|
||||
|
||||
stateStreamContinuation.yield(.joiningServer(server))
|
||||
|
||||
let instance = try await APIService.shared.instance(domain: server.domain, authenticationBox: nil)
|
||||
let instance: RegistrationInstance
|
||||
do {
|
||||
instance = try await APIService.shared.instanceV2(domain: server.domain, authenticationBox: nil)
|
||||
} catch {
|
||||
instance = try await APIService.shared.instance(domain: server.domain, authenticationBox: nil)
|
||||
if instance.isBeyondVersion1 {
|
||||
throw APIService.APIError.explicit(.badResponse)
|
||||
}
|
||||
}
|
||||
|
||||
guard instance.registrations != false else {
|
||||
guard instance.isOpenToNewRegistrations ?? true else {
|
||||
throw AuthenticationViewModel.AuthenticationError.registrationClosed
|
||||
}
|
||||
let application = try await APIService.shared.createApplication(domain: server.domain)
|
||||
@ -157,10 +165,11 @@ extension AuthenticationViewModel {
|
||||
assert(hasAgreedToRules == true)
|
||||
let query = Mastodon.API.Account.RegisterQuery(
|
||||
reason: info.reason,
|
||||
dateOfBirth: info.minAge == nil ? nil : info.dateOfBirth,
|
||||
username: info.username,
|
||||
email: info.email,
|
||||
password: info.password,
|
||||
agreement: hasAgreedToRules,
|
||||
agreement: hasAgreedToRules,
|
||||
locale: locale ?? self.locale
|
||||
)
|
||||
|
||||
|
@ -49,22 +49,39 @@ extension Mastodon.API.Account {
|
||||
|
||||
public struct RegisterQuery: Codable, PostQuery {
|
||||
public let reason: String?
|
||||
public let dateOfBirth: String?
|
||||
public let username: String
|
||||
public let email: String
|
||||
public let password: String
|
||||
public let agreement: Bool
|
||||
public let locale: String
|
||||
|
||||
public init(reason: String? = nil, username: String, email: String, password: String, agreement: Bool, locale: String) {
|
||||
public init(reason: String? = nil, dateOfBirth: Date?, username: String, email: String, password: String, agreement: Bool, locale: String) {
|
||||
self.reason = reason
|
||||
if let dateOfBirth {
|
||||
let dateFormatter = ISO8601DateFormatter()
|
||||
dateFormatter.formatOptions = .withFullDate // YYYY-MM-DD
|
||||
self.dateOfBirth = dateFormatter.string(from: dateOfBirth)
|
||||
} else {
|
||||
self.dateOfBirth = nil
|
||||
}
|
||||
self.username = username
|
||||
self.email = email
|
||||
self.password = password
|
||||
self.agreement = agreement
|
||||
self.locale = locale
|
||||
}
|
||||
|
||||
public enum CodingKeys: String, CodingKey {
|
||||
case reason
|
||||
case dateOfBirth = "date_of_birth"
|
||||
case username
|
||||
case email
|
||||
case password
|
||||
case agreement
|
||||
case locale
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Mastodon.API.Account {
|
||||
|
Loading…
x
Reference in New Issue
Block a user