From 1f922aa478bd0d27226f8ec2f276510d4a1d03fc Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 15 Feb 2019 14:19:12 -0500 Subject: [PATCH] Sketch out the 'onboarding code verification' view. --- .../OnboardingCaptchaViewController.swift | 2 +- .../OnboardingPhoneNumberViewController.swift | 4 +- ...OnboardingVerificationViewController.swift | 687 ++++++++++-------- .../translations/en.lproj/Localizable.strings | 41 +- SignalMessaging/Views/OWSFlatButton.swift | 9 +- SignalMessaging/categories/UIFont+OWS.h | 1 + SignalMessaging/categories/UIFont+OWS.m | 6 + SignalServiceKit/src/Util/String+SSK.swift | 14 + 8 files changed, 447 insertions(+), 317 deletions(-) diff --git a/Signal/src/ViewControllers/Registration/OnboardingCaptchaViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingCaptchaViewController.swift index 9f1f60103..aace0845b 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingCaptchaViewController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingCaptchaViewController.swift @@ -114,7 +114,7 @@ public class OnboardingCaptchaViewController: OnboardingBaseViewController { } onboardingController.update(captchaToken: captchaToken) - onboardingController.tryToRegister(fromViewController: self, smsVerification: false) + onboardingController.tryToRegister(fromViewController: self, smsVerification: true) } private func parseCaptcha(url: URL) -> String? { diff --git a/Signal/src/ViewControllers/Registration/OnboardingPhoneNumberViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingPhoneNumberViewController.swift index b3820df34..9ea1f70c6 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingPhoneNumberViewController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingPhoneNumberViewController.swift @@ -368,10 +368,10 @@ public class OnboardingPhoneNumberViewController: OnboardingBaseViewController { proceedTitle: NSLocalizedString("REGISTRATION_IPAD_CONFIRM_BUTTON", comment: "button text to proceed with registration when on an iPad"), proceedAction: { (_) in - self.onboardingController.tryToRegister(fromViewController: self, smsVerification: false) + self.onboardingController.tryToRegister(fromViewController: self, smsVerification: true) }) } else { - onboardingController.tryToRegister(fromViewController: self, smsVerification: false) + onboardingController.tryToRegister(fromViewController: self, smsVerification: true) } } } diff --git a/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift index bba81af9b..31c7f1b36 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift @@ -5,79 +5,264 @@ import UIKit import PromiseKit +private protocol OnboardingCodeViewTextFieldDelegate { + func textFieldDidDeletePrevious() +} + +// MARK: - + +// Editing a code should feel seamless, as even though +// the UITextField only lets you edit a single digit at +// a time. For deletes to work properly, we need to +// detect delete events that would affect the _previous_ +// digit. +private class OnboardingCodeViewTextField: UITextField { + + fileprivate var codeDelegate: OnboardingCodeViewTextFieldDelegate? + + override func deleteBackward() { + var isDeletePrevious = false + if let selectedTextRange = selectedTextRange { + let cursorPosition = offset(from: beginningOfDocument, to: selectedTextRange.start) + if cursorPosition == 0 { + isDeletePrevious = true + } + } + + super.deleteBackward() + + if isDeletePrevious { + codeDelegate?.textFieldDidDeletePrevious() + } + } + +} + +// MARK: - + +protocol OnboardingCodeViewDelegate { + func codeViewDidChange() +} + +// MARK: - + +// The OnboardingCodeView is a special "verification code" +// editor that should feel like editing a single piece +// of text (ala UITextField) even though the individual +// digits of the code are visually separated. +// +// We use a separate UILabel for each digit, and move +// around a single UITextfield to let the user edit the +// last/next digit. private class OnboardingCodeView: UIView { + + var delegate: OnboardingCodeViewDelegate? + + public init() { + super.init(frame: .zero) + + createSubviews() + + updateViewState() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private let digitCount = 6 + private var digitLabels = [UILabel]() + + // We use a single text field to edit the "current" digit. + // The "current" digit is usually the "last" + fileprivate let textfield = OnboardingCodeViewTextField() + private var currentDigitIndex = 0 + private var textfieldConstraints = [NSLayoutConstraint]() + + // The current complete text - the "model" for this view. + private var digitText = "" + + var isComplete: Bool { + return digitText.count == digitCount + } + + private func createSubviews() { + textfield.textAlignment = .left + textfield.delegate = self + textfield.keyboardType = .numberPad + textfield.textColor = Theme.primaryColor + textfield.font = UIFont.ows_dynamicTypeLargeTitle1Clamped + textfield.codeDelegate = self + + var digitViews = [UIView]() + (0.. (UIView, UILabel) { + let digitView = UIView() + + let digitLabel = UILabel() + digitLabel.text = text + digitLabel.font = UIFont.ows_dynamicTypeLargeTitle1Clamped + digitLabel.textColor = Theme.primaryColor + digitLabel.textAlignment = .center + digitView.addSubview(digitLabel) + digitLabel.autoCenterInSuperview() + + if hasStroke { + let strokeView = UIView.container() + strokeView.backgroundColor = Theme.primaryColor + digitView.addSubview(strokeView) + strokeView.autoPinWidthToSuperview() + strokeView.autoPinEdge(toSuperviewEdge: .bottom) + strokeView.autoSetDimension(.height, toSize: 1) + } + + let vMargin: CGFloat = 4 + let cellHeight: CGFloat = digitLabel.font.lineHeight + vMargin * 2 + let cellWidth: CGFloat = cellHeight * 2 / 3 + digitView.autoSetDimensions(to: CGSize(width: cellWidth, height: cellHeight)) + + return (digitView, digitLabel) + } + + private func digit(at index: Int) -> String { + guard index < digitText.count else { + return "" + } + return digitText.substring(from: index).trim(after: 1) + } + + // Ensure that all labels are displaying the correct + // digit (if any) and that the UITextField has replaced + // the "current" digit. + private func updateViewState() { + currentDigitIndex = min(digitCount - 1, + digitText.count) + + (0.. Bool { + return textfield.becomeFirstResponder() + } +} + +// MARK: - + +extension OnboardingCodeView: UITextFieldDelegate { + public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString newString: String) -> Bool { + var oldText = "" + if let textFieldText = textField.text { + oldText = textFieldText + } + let left = oldText.substring(to: range.location) + let right = oldText.substring(from: range.location + range.length) + let unfiltered = left + newString + right + let characterSet = CharacterSet(charactersIn: "0123456789") + let filtered = unfiltered.components(separatedBy: characterSet.inverted).joined() + let filteredAndTrimmed = filtered.trim(after: 1) + textField.text = filteredAndTrimmed + + digitText = digitText.trim(after: currentDigitIndex) + filteredAndTrimmed + + updateViewState() + + // Inform our caller that we took care of performing the change. + return false + } + + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + return false + } } +// MARK: - + +extension OnboardingCodeView: OnboardingCodeViewTextFieldDelegate { + public func textFieldDidDeletePrevious() { + guard digitText.count > 0 else { + return + } + digitText = digitText.substring(to: currentDigitIndex - 1) + + updateViewState() + } +} + +// MARK: - + @objc public class OnboardingVerificationViewController: OnboardingBaseViewController { -// // MARK: - Dependencies -// -// private var tsAccountManager: TSAccountManager { -// return TSAccountManager.sharedInstance() -// } + private enum CodeState { + case pending + case possiblyNotDelivered + case resent + } // MARK: - + private var codeState = CodeState.pending + + private var titleLabel: UILabel? private let phoneNumberTextField = UITextField() -// private var nextButton: OWSFlatButton? - private var resendCodeLabel: OWSFlatButton? - private var resendCodeLink: OWSFlatButton? + private let onboardingCodeView = OnboardingCodeView() + private var codeStateLink: OWSFlatButton? override public func loadView() { super.loadView() -// populateDefaults() - view.backgroundColor = Theme.backgroundColor view.layoutMargins = .zero - var e164PhoneNumber = "" - if let phoneNumber = onboardingController.phoneNumber { - e164PhoneNumber = phoneNumber.e164 - } - let formattedPhoneNumber = PhoneNumber.bestEffortLocalizedPhoneNumber(withE164: e164PhoneNumber) - let titleText = String(format: NSLocalizedString("ONBOARDING_VERIFICATION_TITLE_FORMAT", - comment: "Format for the title of the 'onboarding verification' view. Embeds {{the user's phone number}}."), - formattedPhoneNumber) - let titleLabel = self.titleLabel(text: titleText) + let titleLabel = self.titleLabel(text: "") + self.titleLabel = titleLabel let backLink = self.linkButton(title: NSLocalizedString("ONBOARDING_VERIFICATION_BACK_LINK", comment: "Label for the link that lets users change their phone number."), selector: #selector(backLinkTapped)) - let onboardingCodeView = OnboardingCodeView() - onboardingCodeView.addRedBorder() - -// resendCodeLabel.text = NSLocalizedString("ONBOARDING_VERIFICATION_BACK_LINK", -// comment: "Label for the link that lets users change their phone number."), -// resendCodeLabel.text = "TODO" -// resendCodeLabel.textColor = Theme.secondaryColor -// resendCodeLabel.font = UIFont.ows_dynamicTypeBodyClamped - - // TODO: Copy. - let resendCodeLabel = disabledLinkButton(title: NSLocalizedString("ONBOARDING_VERIFICATION_RESEND_CODE_LINK", - comment: "Label for the link that lets users request another verification code."), - selector: #selector(ignoreEvent)) - self.resendCodeLabel = resendCodeLabel - - let resendCodeLink = self.linkButton(title: NSLocalizedString("ONBOARDING_VERIFICATION_RESEND_CODE_LINK", - comment: "Label for the link that lets users request another verification code."), + let codeStateLink = self.linkButton(title: "", selector: #selector(resendCodeLinkTapped)) - self.resendCodeLink = resendCodeLink - - let resentCodeWrapper = UIView.container() - resentCodeWrapper.addSubview(resendCodeLabel) - resentCodeWrapper.addSubview(resendCodeLink) - resendCodeLabel.autoPinEdgesToSuperviewEdges() - resendCodeLink.autoPinEdgesToSuperviewEdges() - - // TODO: Finalize copy. + codeStateLink.enableMultilineLabel() + self.codeStateLink = codeStateLink -// let nextButton = self.button(title: NSLocalizedString("BUTTON_NEXT", -// comment: "Label for the 'next' button."), -// selector: #selector(nextPressed)) -// self.nextButton = nextButton let topSpacer = UIView.vStretchingSpacer() let bottomSpacer = UIView.vStretchingSpacer() @@ -87,11 +272,8 @@ public class OnboardingVerificationViewController: OnboardingBaseViewController backLink, topSpacer, onboardingCodeView, -// countryRow, -// UIView.spacer(withHeight: 8), -// phoneNumberRow, bottomSpacer, - resentCodeWrapper + codeStateLink ]) stackView.axis = .vertical stackView.alignment = .fill @@ -104,209 +286,122 @@ public class OnboardingVerificationViewController: OnboardingBaseViewController // Ensure whitespace is balanced, so inputs are vertically centered. topSpacer.autoMatch(.height, to: .height, of: bottomSpacer) + + startCodeCountdown() + + updateCodeState() } -// private func addBottomStroke(_ view: UIView) { -// let strokeView = UIView() -// strokeView.backgroundColor = Theme.middleGrayColor -// view.addSubview(strokeView) -// strokeView.autoSetDimension(.height, toSize: CGHairlineWidth()) -// strokeView.autoPinWidthToSuperview() -// strokeView.autoPinEdge(toSuperviewEdge: .bottom) -// } -// -// public override func viewDidAppear(_ animated: Bool) { -// super.viewDidAppear(animated) -// -// phoneNumberTextField.becomeFirstResponder() -// -// if tsAccountManager.isReregistering() { -// // If re-registering, pre-populate the country (country code, calling code, country name) -// // and phone number state. -// guard let phoneNumberE164 = tsAccountManager.reregisterationPhoneNumber() else { -// owsFailDebug("Could not resume re-registration; missing phone number.") -// return -// } -// tryToReregister(phoneNumberE164: phoneNumberE164) -// } -// } -// -// private func tryToReregister(phoneNumberE164: String) { -// guard phoneNumberE164.count > 0 else { -// owsFailDebug("Could not resume re-registration; invalid phoneNumberE164.") -// return -// } -// guard let parsedPhoneNumber = PhoneNumber(fromE164: phoneNumberE164) else { -// owsFailDebug("Could not resume re-registration; couldn't parse phoneNumberE164.") -// return -// } -// guard let callingCodeNumeric = parsedPhoneNumber.getCountryCode() else { -// owsFailDebug("Could not resume re-registration; missing callingCode.") -// return -// } -// let callingCode = "\(COUNTRY_CODE_PREFIX)\(callingCodeNumeric)" -// let countryCodes: [String] = -// PhoneNumberUtil.sharedThreadLocal().countryCodes(fromCallingCode: callingCode) -// guard let countryCode = countryCodes.first else { -// owsFailDebug("Could not resume re-registration; unknown countryCode.") -// return -// } -// guard let countryName = PhoneNumberUtil.countryName(fromCountryCode: countryCode) else { -// owsFailDebug("Could not resume re-registration; unknown countryName.") -// return -// } -// if !phoneNumberE164.hasPrefix(callingCode) { -// owsFailDebug("Could not resume re-registration; non-matching calling code.") -// return -// } -// let phoneNumberWithoutCallingCode = phoneNumberE164.substring(from: callingCode.count) -// -// guard countryCode.count > 0 else { -// owsFailDebug("Invalid country code.") -// return -// } -// guard countryName.count > 0 else { -// owsFailDebug("Invalid country name.") -// return -// } -// guard callingCode.count > 0 else { -// owsFailDebug("Invalid calling code.") -// return -// } -// -// let countryState = OnboardingCountryState(countryName: countryName, callingCode: callingCode, countryCode: countryCode) -// onboardingController.update(countryState: countryState) -// -// updateState() -// -// phoneNumberTextField.text = phoneNumberWithoutCallingCode -// // Don't let user edit their phone number while re-registering. -// phoneNumberTextField.isEnabled = false -// } -// -// // MARK: - -// -// private var countryName: String { -// get { -// return onboardingController.countryState.countryName -// } -// } -// private var callingCode: String { -// get { -// AssertIsOnMainThread() -// -// return onboardingController.countryState.callingCode -// } -// } -// private var countryCode: String { -// get { -// AssertIsOnMainThread() -// -// return onboardingController.countryState.countryCode -// } -// } -// -// private func populateDefaults() { -// if let lastRegisteredPhoneNumber = OnboardingController.lastRegisteredPhoneNumber(), -// lastRegisteredPhoneNumber.count > 0, -// lastRegisteredPhoneNumber.hasPrefix(callingCode) { -// phoneNumberTextField.text = lastRegisteredPhoneNumber.substring(from: callingCode.count) -// } else if let phoneNumber = onboardingController.phoneNumber { -// phoneNumberTextField.text = phoneNumber.userInput -// } -// -// updateState() -// } -// -// private func updateState() { -// AssertIsOnMainThread() -// -// countryNameLabel.text = countryName -// callingCodeLabel.text = callingCode -// -// self.phoneNumberTextField.placeholder = ViewControllerUtils.examplePhoneNumber(forCountryCode: countryCode, callingCode: callingCode) -// } -// -// // MARK: - Events -// -// @objc func countryRowTapped(sender: UIGestureRecognizer) { -// guard sender.state == .recognized else { -// return -// } -// showCountryPicker() -// } -// -// @objc func countryCodeTapped(sender: UIGestureRecognizer) { -// guard sender.state == .recognized else { -// return -// } -// showCountryPicker() -// } -// -// @objc func nextPressed() { -// Logger.info("") -// -// parseAndTryToRegister() -// } -// -// // MARK: - Country Picker -// -// private func showCountryPicker() { -// guard !tsAccountManager.isReregistering() else { -// return -// } -// -// let countryCodeController = CountryCodeViewController() -// countryCodeController.countryCodeDelegate = self -// countryCodeController.interfaceOrientationMask = .portrait -// let navigationController = OWSNavigationController(rootViewController: countryCodeController) -// self.present(navigationController, animated: true, completion: nil) -// } -// -// // MARK: - Register -// -// private func parseAndTryToRegister() { -// guard let phoneNumberText = phoneNumberTextField.text?.ows_stripped(), -// phoneNumberText.count > 0 else { -// OWSAlerts.showAlert(title: -// NSLocalizedString("REGISTRATION_VIEW_NO_VERIFICATION_ALERT_TITLE", -// comment: "Title of alert indicating that users needs to enter a phone number to register."), -// message: -// NSLocalizedString("REGISTRATION_VIEW_NO_VERIFICATION_ALERT_MESSAGE", -// comment: "Message of alert indicating that users needs to enter a phone number to register.")) -// return -// } -// -// let phoneNumber = "\(callingCode)\(phoneNumberText)" -// guard let localNumber = PhoneNumber.tryParsePhoneNumber(fromUserSpecifiedText: phoneNumber), -// localNumber.toE164().count > 0, -// PhoneNumberValidator().isValidForRegistration(phoneNumber: localNumber) else { -// OWSAlerts.showAlert(title: -// NSLocalizedString("REGISTRATION_VIEW_INVALID_VERIFICATION_ALERT_TITLE", -// comment: "Title of alert indicating that users needs to enter a valid phone number to register."), -// message: -// NSLocalizedString("REGISTRATION_VIEW_INVALID_VERIFICATION_ALERT_MESSAGE", -// comment: "Message of alert indicating that users needs to enter a valid phone number to register.")) -// return -// } -// let e164PhoneNumber = localNumber.toE164() -// -// onboardingController.update(phoneNumber: OnboardingPhoneNumber(e164: e164PhoneNumber, userInput: phoneNumberText)) -// -// if UIDevice.current.isIPad { -// OWSAlerts.showConfirmationAlert(title: NSLocalizedString("REGISTRATION_IPAD_CONFIRM_TITLE", -// comment: "alert title when registering an iPad"), -// message: NSLocalizedString("REGISTRATION_IPAD_CONFIRM_BODY", -// comment: "alert body when registering an iPad"), -// proceedTitle: NSLocalizedString("REGISTRATION_IPAD_CONFIRM_BUTTON", -// comment: "button text to proceed with registration when on an iPad"), -// proceedAction: { (_) in -// self.onboardingController.tryToRegister(fromViewController: self, smsVerification: false) -// }) -// } else { -// onboardingController.tryToRegister(fromViewController: self, smsVerification: false) -// } -// } + // MARK: - Code State + + private let countdownDuration: TimeInterval = 60 + private var codeCountdownTimer: Timer? + private var codeCountdownStart: NSDate? + + deinit { + if let codeCountdownTimer = codeCountdownTimer { + codeCountdownTimer.invalidate() + } + } + + private func startCodeCountdown() { + codeCountdownStart = NSDate() + codeCountdownTimer = Timer.weakScheduledTimer(withTimeInterval: 1, target: self, selector: #selector(codeCountdownTimerFired), userInfo: nil, repeats: true) + } + + @objc + public func codeCountdownTimerFired() { + guard let codeCountdownStart = codeCountdownStart else { + owsFailDebug("Missing codeCountdownStart.") + return + } + guard let codeCountdownTimer = codeCountdownTimer else { + owsFailDebug("Missing codeCountdownTimer.") + return + } + + let countdownInterval = abs(codeCountdownStart.timeIntervalSinceNow) + + guard countdownInterval < countdownDuration else { + // Countdown complete. + codeCountdownTimer.invalidate() + self.codeCountdownTimer = nil + + if codeState != .pending { + owsFailDebug("Unexpected codeState: \(codeState)") + } + codeState = .possiblyNotDelivered + updateCodeState() + return + } + + // Update the "code state" UI to reflect the countdown. + updateCodeState() + } + + private func updateCodeState() { + AssertIsOnMainThread() + + guard let codeCountdownStart = codeCountdownStart else { + owsFailDebug("Missing codeCountdownStart.") + return + } + guard let titleLabel = titleLabel else { + owsFailDebug("Missing titleLabel.") + return + } + guard let codeStateLink = codeStateLink else { + owsFailDebug("Missing codeStateLink.") + return + } + + var e164PhoneNumber = "" + if let phoneNumber = onboardingController.phoneNumber { + e164PhoneNumber = phoneNumber.e164 + } + let formattedPhoneNumber = PhoneNumber.bestEffortLocalizedPhoneNumber(withE164: e164PhoneNumber) + + // Update titleLabel + switch codeState { + case .pending, .possiblyNotDelivered: + titleLabel.text = String(format: NSLocalizedString("ONBOARDING_VERIFICATION_TITLE_DEFAULT_FORMAT", + comment: "Format for the title of the 'onboarding verification' view. Embeds {{the user's phone number}}."), + formattedPhoneNumber) + case .resent: + titleLabel.text = String(format: NSLocalizedString("ONBOARDING_VERIFICATION_TITLE_RESENT_FORMAT", + comment: "Format for the title of the 'onboarding verification' view after the verification code has been resent. Embeds {{the user's phone number}}."), + formattedPhoneNumber) + } + + // Update codeStateLink + switch codeState { + case .pending: + let countdownInterval = abs(codeCountdownStart.timeIntervalSinceNow) + let countdownRemaining = max(0, countdownDuration - countdownInterval) + let formattedCountdown = OWSFormat.formatDurationSeconds(Int(round(countdownRemaining))) + let text = String(format: NSLocalizedString("ONBOARDING_VERIFICATION_CODE_COUNTDOWN_FORMAT", + comment: "Format for the label of the 'pending code' label of the 'onboarding verification' view. Embeds {{the time until the code can be resent}}."), + formattedCountdown) + codeStateLink.setTitle(title: text, font: .ows_dynamicTypeBodyClamped, titleColor: Theme.secondaryColor) +// codeStateLink.setBackgroundColors(upColor: Theme.backgroundColor) + case .possiblyNotDelivered: + codeStateLink.setTitle(title: NSLocalizedString("ONBOARDING_VERIFICATION_ORIGINAL_CODE_MISSING_LINK", + comment: "Label for link that can be used when the original code did not arrive."), + font: .ows_dynamicTypeBodyClamped, + titleColor: .ows_materialBlue) + case .resent: + codeStateLink.setTitle(title: NSLocalizedString("ONBOARDING_VERIFICATION_RESENT_CODE_MISSING_LINK", + comment: "Label for link that can be used when the resent code did not arrive."), + font: .ows_dynamicTypeBodyClamped, + titleColor: .ows_materialBlue) + } + } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + _ = onboardingCodeView.becomeFirstResponder() + } // MARK: - Events @@ -323,52 +418,44 @@ public class OnboardingVerificationViewController: OnboardingBaseViewController @objc func resendCodeLinkTapped() { Logger.info("") - // TODO: -// self.navigationController?.popViewController(animated: true) + switch codeState { + case .pending: + // Ignore taps until the countdown expires. + break + case .possiblyNotDelivered, .resent: + showResendActionSheet() + } + } + + private func showResendActionSheet() { + Logger.info("") + + let actionSheet = UIAlertController(title: NSLocalizedString("ONBOARDING_VERIFICATION_RESEND_CODE_ALERT_TITLE", + comment: "Title for the 'resend code' alert in the 'onboarding verification' view."), + message: NSLocalizedString("ONBOARDING_VERIFICATION_RESEND_CODE_ALERT_MESSAGE", + comment: "Message for the 'resend code' alert in the 'onboarding verification' view."), + preferredStyle: .actionSheet) + + actionSheet.addAction(UIAlertAction(title: NSLocalizedString("ONBOARDING_VERIFICATION_RESEND_CODE_BY_SMS_BUTTON", + comment: "Label for the 'resend code by SMS' button in the 'onboarding verification' view."), + style: .default) { _ in + self.onboardingController.tryToRegister(fromViewController: self, smsVerification: true) + }) + actionSheet.addAction(UIAlertAction(title: NSLocalizedString("ONBOARDING_VERIFICATION_RESEND_CODE_BY_VOICE_BUTTON", + comment: "Label for the 'resend code by voice' button in the 'onboarding verification' view."), + style: .default) { _ in + self.onboardingController.tryToRegister(fromViewController: self, smsVerification: false) + }) + actionSheet.addAction(OWSAlerts.cancelAction) + + self.present(actionSheet, animated: true) } } -//// MARK: - -// -//extension OnboardingVerificationViewController: UITextFieldDelegate { -// public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { -// // TODO: Fix auto-format of phone numbers. -// ViewControllerUtils.phoneNumber(textField, shouldChangeCharactersIn: range, replacementString: string, countryCode: countryCode) -// -// // Inform our caller that we took care of performing the change. -// return false -// } -// -// public func textFieldShouldReturn(_ textField: UITextField) -> Bool { -// parseAndTryToRegister() -// return false -// } -//} -// -//// MARK: - -// -//extension OnboardingVerificationViewController: CountryCodeViewControllerDelegate { -// public func countryCodeViewController(_ vc: CountryCodeViewController, didSelectCountryCode countryCode: String, countryName: String, callingCode: String) { -// guard countryCode.count > 0 else { -// owsFailDebug("Invalid country code.") -// return -// } -// guard countryName.count > 0 else { -// owsFailDebug("Invalid country name.") -// return -// } -// guard callingCode.count > 0 else { -// owsFailDebug("Invalid calling code.") -// return -// } -// -// let countryState = OnboardingCountryState(countryName: countryName, callingCode: callingCode, countryCode: countryCode) -// -// onboardingController.update(countryState: countryState) -// -// updateState() -// -// // Trigger the formatting logic with a no-op edit. -// _ = textField(phoneNumberTextField, shouldChangeCharactersIn: NSRange(location: 0, length: 0), replacementString: "") -// } -//} +// MARK: - + +extension OnboardingVerificationViewController: OnboardingCodeViewDelegate { + public func codeViewDidChange() { + // TODO: + } +} diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index eb6c9dbb8..06695486d 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1538,8 +1538,35 @@ /* Label for the link that lets users change their phone number. */ "ONBOARDING_VERIFICATION_BACK_LINK" = "Wrong number?"; +/* Format for the label of the 'pending code' label of the 'onboarding verification' view. Embeds {{the time until the code can be resent}}. */ +"ONBOARDING_VERIFICATION_CODE_COUNTDOWN_FORMAT" = "I didn't get a code (available in %@)"; + +/* Label for link that can be used when the original code did not arrive. */ +"ONBOARDING_VERIFICATION_ORIGINAL_CODE_MISSING_LINK" = "I didn't get a code"; + +/* Message for the 'resend code' alert in the 'onboarding verification' view. */ +"ONBOARDING_VERIFICATION_RESEND_CODE_ALERT_MESSAGE" = "Please ensure that you have cellular service and can receive SMS messages."; + +/* Title for the 'resend code' alert in the 'onboarding verification' view. */ +"ONBOARDING_VERIFICATION_RESEND_CODE_ALERT_TITLE" = "No code?"; + +/* Label for the 'resend code by SMS' button in the 'onboarding verification' view. */ +"ONBOARDING_VERIFICATION_RESEND_CODE_BY_SMS_BUTTON" = "Resend code"; + +/* Label for the 'resend code by voice' button in the 'onboarding verification' view. */ +"ONBOARDING_VERIFICATION_RESEND_CODE_BY_VOICE_BUTTON" = "Call me instead"; + +/* Label for the link that lets users request another verification code. */ +"ONBOARDING_VERIFICATION_RESEND_CODE_LINK" = "ONBOARDING_VERIFICATION_RESEND_CODE_LINK"; + +/* Label for link that can be used when the resent code did not arrive. */ +"ONBOARDING_VERIFICATION_RESENT_CODE_MISSING_LINK" = "Still no code?"; + /* Format for the title of the 'onboarding verification' view. Embeds {{the user's phone number}}. */ -"ONBOARDING_VERIFICATION_TITLE_FORMAT" = "Enter the code we sent to %@"; +"ONBOARDING_VERIFICATION_TITLE_DEFAULT_FORMAT" = "Enter the code we sent to %@"; + +/* Format for the title of the 'onboarding verification' view after the verification code has been resent. Embeds {{the user's phone number}}. */ +"ONBOARDING_VERIFICATION_TITLE_RESENT_FORMAT" = "We just resent a code to %@"; /* Button text which opens the settings app */ "OPEN_SETTINGS_BUTTON" = "Settings"; @@ -1841,24 +1868,12 @@ /* Title of alert indicating that users needs to enter a valid phone number to register. */ "REGISTRATION_VIEW_INVALID_PHONE_NUMBER_ALERT_TITLE" = "Invalid Phone Number"; -/* Message of alert indicating that users needs to enter a valid phone number to register. */ -"REGISTRATION_VIEW_INVALID_VERIFICATION_ALERT_MESSAGE" = "REGISTRATION_VIEW_INVALID_VERIFICATION_ALERT_MESSAGE"; - -/* Title of alert indicating that users needs to enter a valid phone number to register. */ -"REGISTRATION_VIEW_INVALID_VERIFICATION_ALERT_TITLE" = "REGISTRATION_VIEW_INVALID_VERIFICATION_ALERT_TITLE"; - /* Message of alert indicating that users needs to enter a phone number to register. */ "REGISTRATION_VIEW_NO_PHONE_NUMBER_ALERT_MESSAGE" = "Please enter a phone number to register."; /* Title of alert indicating that users needs to enter a phone number to register. */ "REGISTRATION_VIEW_NO_PHONE_NUMBER_ALERT_TITLE" = "No Phone Number"; -/* Message of alert indicating that users needs to enter a phone number to register. */ -"REGISTRATION_VIEW_NO_VERIFICATION_ALERT_MESSAGE" = "REGISTRATION_VIEW_NO_VERIFICATION_ALERT_MESSAGE"; - -/* Title of alert indicating that users needs to enter a phone number to register. */ -"REGISTRATION_VIEW_NO_VERIFICATION_ALERT_TITLE" = "REGISTRATION_VIEW_NO_VERIFICATION_ALERT_TITLE"; - /* notification action */ "REJECT_CALL_BUTTON_TITLE" = "Reject"; diff --git a/SignalMessaging/Views/OWSFlatButton.swift b/SignalMessaging/Views/OWSFlatButton.swift index 7337aed5d..134ccdef9 100644 --- a/SignalMessaging/Views/OWSFlatButton.swift +++ b/SignalMessaging/Views/OWSFlatButton.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // import Foundation @@ -167,4 +167,11 @@ public class OWSFlatButton: UIView { internal func buttonPressed() { pressedBlock?() } + + @objc + public func enableMultilineLabel() { + button.titleLabel?.numberOfLines = 0 + button.titleLabel?.lineBreakMode = .byWordWrapping + button.titleLabel?.textAlignment = .center + } } diff --git a/SignalMessaging/categories/UIFont+OWS.h b/SignalMessaging/categories/UIFont+OWS.h index b0bcda463..88da5beb9 100644 --- a/SignalMessaging/categories/UIFont+OWS.h +++ b/SignalMessaging/categories/UIFont+OWS.h @@ -38,6 +38,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Dynamic Type Clamped +@property (class, readonly, nonatomic) UIFont *ows_dynamicTypeLargeTitle1ClampedFont; @property (class, readonly, nonatomic) UIFont *ows_dynamicTypeTitle1ClampedFont; @property (class, readonly, nonatomic) UIFont *ows_dynamicTypeTitle2ClampedFont; @property (class, readonly, nonatomic) UIFont *ows_dynamicTypeTitle3ClampedFont; diff --git a/SignalMessaging/categories/UIFont+OWS.m b/SignalMessaging/categories/UIFont+OWS.m index a7a1a4451..2d13b5dc6 100644 --- a/SignalMessaging/categories/UIFont+OWS.m +++ b/SignalMessaging/categories/UIFont+OWS.m @@ -107,6 +107,7 @@ NS_ASSUME_NONNULL_BEGIN static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ maxPointSizeMap = @{ + UIFontTextStyleLargeTitle : @(40.0), UIFontTextStyleTitle1 : @(34.0), UIFontTextStyleTitle2 : @(28.0), UIFontTextStyleTitle3 : @(26.0), @@ -132,6 +133,11 @@ NS_ASSUME_NONNULL_BEGIN return font; } ++ (UIFont *)ows_dynamicTypeLargeTitle1ClampedFont +{ + return [UIFont preferredFontForTextStyleClamped:UIFontTextStyleLargeTitle]; +} + + (UIFont *)ows_dynamicTypeTitle1ClampedFont { return [UIFont preferredFontForTextStyleClamped:UIFontTextStyleTitle1]; diff --git a/SignalServiceKit/src/Util/String+SSK.swift b/SignalServiceKit/src/Util/String+SSK.swift index b3bdca923..6312dd677 100644 --- a/SignalServiceKit/src/Util/String+SSK.swift +++ b/SignalServiceKit/src/Util/String+SSK.swift @@ -5,6 +5,10 @@ import Foundation public extension String { + public var digitsOnly: String { + return (self as NSString).digitsOnly() + } + func rtlSafeAppend(_ string: String) -> String { return (self as NSString).rtlSafeAppend(string) } @@ -12,4 +16,14 @@ public extension String { public func substring(from index: Int) -> String { return String(self[self.index(self.startIndex, offsetBy: index)...]) } + + public func substring(to index: Int) -> String { + return String(self[.. String { + let index = min(maxCount, self.count) + return substring(to: index) + } }