// // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import SignalServiceKit public enum RegistrationPhoneNumberViewState: Equatable { case registration(RegistrationMode) case changingNumber(ChangingNumberMode) public enum RegistrationMode: Equatable { case initialRegistration(InitialRegistration) case reregistration(Reregistration) } public enum ChangingNumberMode: Equatable { case initialEntry(ChangeNumberInitialEntry) case confirmation(ChangeNumberConfirmation) } public struct InitialRegistration: Equatable { /// previouslyEnteredE164 is if the user entered a number, quit, and came back. /// Will be used to pre-populate the entry field. let previouslyEnteredE164: E164? let validationError: ValidationError? let canExitRegistration: Bool } public struct Reregistration: Equatable { let e164: E164 let validationError: ValidationError? let canExitRegistration: Bool } public struct ChangeNumberInitialEntry: Equatable { let oldE164: E164 let newE164: E164? let hasConfirmed: Bool let invalidE164Error: ValidationError.InvalidE164? } public struct ChangeNumberConfirmation: Equatable { let oldE164: E164 let newE164: E164 let rateLimitedError: ValidationError.RateLimited? } public enum ValidationError: Equatable { case invalidInput(InvalidInput) case invalidE164(InvalidE164) case rateLimited(RateLimited) /// The user typed something that couldn't be parsed. public struct InvalidInput: Equatable { let invalidCountryCode: String let invalidNationalNumber: String } /// The user submitted something that could be parsed, but local or server /// validation rejected it as not a valid number for registration. public struct InvalidE164: Equatable { let invalidE164: E164 } public struct RateLimited: Equatable { let expiration: Date let e164: E164 } } } extension RegistrationPhoneNumberViewState.ValidationError { func warningLabelText(dateProvider: DateProvider) -> String? { switch self { case let .invalidInput(error): return error.warningLabelText() case let .invalidE164(error): return error.warningLabelText() case let .rateLimited(error): return error.warningLabelText(dateProvider: dateProvider) } } } extension RegistrationPhoneNumberViewState.ValidationError.InvalidInput { func canSubmit(countryCode: String, nationalNumber: String) -> Bool { return countryCode != invalidCountryCode || nationalNumber != invalidNationalNumber } func warningLabelText() -> String { return OWSLocalizedString( "ONBOARDING_PHONE_NUMBER_VALIDATION_WARNING", comment: "Label indicating that the phone number is invalid in the 'onboarding phone number' view." ) } } extension RegistrationPhoneNumberViewState.ValidationError.InvalidE164 { func canSubmit(e164: E164?) -> Bool { return e164 != invalidE164 } func warningLabelText() -> String { return OWSLocalizedString( "ONBOARDING_PHONE_NUMBER_VALIDATION_WARNING", comment: "Label indicating that the phone number is invalid in the 'onboarding phone number' view." ) } } extension RegistrationPhoneNumberViewState.ValidationError.RateLimited { func canSubmit(e164: E164?, dateProvider: DateProvider) -> Bool { return dateProvider() >= expiration || e164 != self.e164 } func warningLabelText(dateProvider: DateProvider) -> String { let now = dateProvider() let rateLimitFormat = OWSLocalizedString( "ONBOARDING_PHONE_NUMBER_RATE_LIMIT_WARNING_FORMAT", comment: "Label indicating that registration has been ratelimited. Embeds {{remaining time string}}." ) let timeRemaining = max(expiration.timeIntervalSince(now), 0) let retryAfterFormatter: DateFormatter = { let result = DateFormatter() // Only display hour if the coundown is greater than 60 min result.dateFormat = timeRemaining > 3600 ? "H:mm:ss" : "m:ss" result.timeZone = TimeZone(identifier: "UTC")! return result }() let durationString = retryAfterFormatter.string(from: Date(timeIntervalSinceReferenceDate: timeRemaining)) return String(format: rateLimitFormat, durationString) } }