From d193eec371f5e9a669976b83370eadfc9b277fe9 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 15 Feb 2019 10:21:19 -0500 Subject: [PATCH 1/7] Sketch out the 'onboarding code verification' view. --- Signal.xcodeproj/project.pbxproj | 4 + .../HomeView/HomeViewController.m | 13 + .../OnboardingBaseViewController.swift | 25 +- .../Registration/OnboardingController.swift | 4 +- ...OnboardingVerificationViewController.swift | 374 ++++++++++++++++++ .../translations/en.lproj/Localizable.strings | 18 + 6 files changed, 422 insertions(+), 16 deletions(-) create mode 100644 Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 40575582f..0b34e517f 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -165,6 +165,7 @@ 3496957321A301A100DCFE74 /* OWSBackupJob.m in Sources */ = {isa = PBXBuildFile; fileRef = 3496956A21A301A100DCFE74 /* OWSBackupJob.m */; }; 3496957421A301A100DCFE74 /* OWSBackupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496956B21A301A100DCFE74 /* OWSBackupAPI.swift */; }; 349EA07C2162AEA800F7B17F /* OWS111UDAttributesMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349EA07B2162AEA700F7B17F /* OWS111UDAttributesMigration.swift */; }; + 34A4C61E221613D00042EF2E /* OnboardingVerificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A4C61D221613D00042EF2E /* OnboardingVerificationViewController.swift */; }; 34A55F3720485465002CC6DE /* OWS2FARegistrationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A55F3520485464002CC6DE /* OWS2FARegistrationViewController.m */; }; 34A6C28021E503E700B5B12E /* OWSImagePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */; }; 34A8B3512190A40E00218A25 /* MediaAlbumCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A8B3502190A40E00218A25 /* MediaAlbumCellView.swift */; }; @@ -846,6 +847,7 @@ 3496956C21A301A100DCFE74 /* OWSBackupImportJob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupImportJob.h; sourceTree = ""; }; 3496956D21A301A100DCFE74 /* OWSBackupIO.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupIO.h; sourceTree = ""; }; 349EA07B2162AEA700F7B17F /* OWS111UDAttributesMigration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWS111UDAttributesMigration.swift; sourceTree = ""; }; + 34A4C61D221613D00042EF2E /* OnboardingVerificationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingVerificationViewController.swift; sourceTree = ""; }; 34A55F3520485464002CC6DE /* OWS2FARegistrationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWS2FARegistrationViewController.m; sourceTree = ""; }; 34A55F3620485464002CC6DE /* OWS2FARegistrationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWS2FARegistrationViewController.h; sourceTree = ""; }; 34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSImagePickerController.swift; sourceTree = ""; }; @@ -1473,6 +1475,7 @@ 3448E15B22133274004B052E /* OnboardingPermissionsViewController.swift */, 3448E16322135FFA004B052E /* OnboardingPhoneNumberViewController.swift */, 3448E15F22134C88004B052E /* OnboardingSplashViewController.swift */, + 34A4C61D221613D00042EF2E /* OnboardingVerificationViewController.swift */, 346E9D5321B040B600562252 /* RegistrationController.swift */, 340FC878204DAC8C007AEB0F /* RegistrationViewController.h */, 340FC876204DAC8C007AEB0F /* RegistrationViewController.m */, @@ -3537,6 +3540,7 @@ 34D1F0521F7E8EA30066283D /* GiphyDownloader.swift in Sources */, 340FC8BC204DAC8D007AEB0F /* FingerprintViewController.m in Sources */, 450DF2051E0D74AC003D14BE /* Platform.swift in Sources */, + 34A4C61E221613D00042EF2E /* OnboardingVerificationViewController.swift in Sources */, 340FC8B2204DAC8D007AEB0F /* AdvancedSettingsTableViewController.m in Sources */, 452B999020A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift in Sources */, 346129991FD1E4DA00532771 /* SignalApp.m in Sources */, diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m index 2e8ab8ba1..88eb9f6f0 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -482,6 +482,19 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations [self.searchResultsController viewDidAppear:animated]; self.hasEverAppeared = YES; + + dispatch_async(dispatch_get_main_queue(), ^{ + OnboardingController *onboardingController = [OnboardingController new]; + [onboardingController + updateWithPhoneNumber:[[OnboardingPhoneNumber alloc] initWithE164:@"+13213214321" userInput:@"3213214321"]]; + + // UIViewController *view = [onboardingController initialViewController]; + UIViewController *view = + [[OnboardingVerificationViewController alloc] initWithOnboardingController:onboardingController]; + OWSNavigationController *navigationController = + [[OWSNavigationController alloc] initWithRootViewController:view]; + [self presentViewController:navigationController animated:YES completion:nil]; + }); } - (void)viewDidDisappear:(BOOL)animated diff --git a/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift index bd76bdb98..ac9125a27 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift @@ -50,29 +50,26 @@ public class OnboardingBaseViewController: OWSViewController { } func button(title: String, selector: Selector) -> OWSFlatButton { - // TODO: Make sure this all fits if dynamic font sizes are maxed out. - let font = UIFont.ows_dynamicTypeBodyClamped.ows_mediumWeight() - // Button height should be 48pt if the font is 17pt. - let buttonHeight = font.pointSize * 48 / 17 - let button = OWSFlatButton.button(title: title, - font: font, - titleColor: .white, - backgroundColor: .ows_materialBlue, - target: self, - selector: selector) - button.autoSetDimension(.height, toSize: buttonHeight) - return button + return button(title: title, selector: selector, titleColor: .white, backgroundColor: .ows_materialBlue) } func linkButton(title: String, selector: Selector) -> OWSFlatButton { + return button(title: title, selector: selector, titleColor: .ows_materialBlue, backgroundColor: .white) + } + + func disabledLinkButton(title: String, selector: Selector) -> OWSFlatButton { + return self.button(title: title, selector: selector, titleColor: Theme.secondaryColor, backgroundColor: .white) + } + + private func button(title: String, selector: Selector, titleColor: UIColor, backgroundColor: UIColor) -> OWSFlatButton { // TODO: Make sure this all fits if dynamic font sizes are maxed out. let font = UIFont.ows_dynamicTypeBodyClamped.ows_mediumWeight() // Button height should be 48pt if the font is 17pt. let buttonHeight = font.pointSize * 48 / 17 let button = OWSFlatButton.button(title: title, font: font, - titleColor: .ows_materialBlue, - backgroundColor: .white, + titleColor: titleColor, + backgroundColor: backgroundColor, target: self, selector: selector) button.autoSetDimension(.height, toSize: buttonHeight) diff --git a/Signal/src/ViewControllers/Registration/OnboardingController.swift b/Signal/src/ViewControllers/Registration/OnboardingController.swift index 532cc975f..5909cd674 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingController.swift @@ -122,8 +122,8 @@ public class OnboardingController: NSObject { Logger.info("") - // CodeVerificationViewController *vc = [CodeVerificationViewController new]; - // [weakSelf.navigationController pushViewController:vc animated:YES]; + let view = OnboardingVerificationViewController(onboardingController: self) + viewController.navigationController?.pushViewController(view, animated: true) } public func onboardingDidRequireCaptcha(viewController: UIViewController) { diff --git a/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift new file mode 100644 index 000000000..bba81af9b --- /dev/null +++ b/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift @@ -0,0 +1,374 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import UIKit +import PromiseKit + +private class OnboardingCodeView: UIView { +} + +@objc +public class OnboardingVerificationViewController: OnboardingBaseViewController { + +// // MARK: - Dependencies +// +// private var tsAccountManager: TSAccountManager { +// return TSAccountManager.sharedInstance() +// } + + // MARK: - + + private let phoneNumberTextField = UITextField() +// private var nextButton: OWSFlatButton? + private var resendCodeLabel: OWSFlatButton? + private var resendCodeLink: 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 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."), + selector: #selector(resendCodeLinkTapped)) + self.resendCodeLink = resendCodeLink + + let resentCodeWrapper = UIView.container() + resentCodeWrapper.addSubview(resendCodeLabel) + resentCodeWrapper.addSubview(resendCodeLink) + resendCodeLabel.autoPinEdgesToSuperviewEdges() + resendCodeLink.autoPinEdgesToSuperviewEdges() + + // TODO: Finalize copy. + +// 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() + + let stackView = UIStackView(arrangedSubviews: [ + titleLabel, + UIView.spacer(withHeight: 21), + backLink, + topSpacer, + onboardingCodeView, +// countryRow, +// UIView.spacer(withHeight: 8), +// phoneNumberRow, + bottomSpacer, + resentCodeWrapper + ]) + stackView.axis = .vertical + stackView.alignment = .fill + stackView.layoutMargins = UIEdgeInsets(top: 32, left: 32, bottom: 32, right: 32) + stackView.isLayoutMarginsRelativeArrangement = true + view.addSubview(stackView) + stackView.autoPinWidthToSuperview() + stackView.autoPin(toTopLayoutGuideOf: self, withInset: 0) + autoPinView(toBottomOfViewControllerOrKeyboard: stackView, avoidNotch: true) + + // Ensure whitespace is balanced, so inputs are vertically centered. + topSpacer.autoMatch(.height, to: .height, of: bottomSpacer) + } + +// 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: - Events + + @objc func backLinkTapped() { + Logger.info("") + + self.navigationController?.popViewController(animated: true) + } + + @objc func ignoreEvent() { + Logger.info("") + } + + @objc func resendCodeLinkTapped() { + Logger.info("") + + // TODO: +// self.navigationController?.popViewController(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: "") +// } +//} diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 9c80faccb..eb6c9dbb8 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1535,6 +1535,12 @@ /* Title of the 'onboarding splash' view. */ "ONBOARDING_SPLASH_TITLE" = "Signal is the private messenger for everybody"; +/* Label for the link that lets users change their phone number. */ +"ONBOARDING_VERIFICATION_BACK_LINK" = "Wrong number?"; + +/* 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 %@"; + /* Button text which opens the settings app */ "OPEN_SETTINGS_BUTTON" = "Settings"; @@ -1835,12 +1841,24 @@ /* 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"; From 1f922aa478bd0d27226f8ec2f276510d4a1d03fc Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 15 Feb 2019 14:19:12 -0500 Subject: [PATCH 2/7] 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) + } } From efe5513c4e9fe1f2ceb1026daa7b41c10b267961 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 15 Feb 2019 14:56:32 -0500 Subject: [PATCH 3/7] Sketch out the 'onboarding code verification' view. --- .../OnboardingBaseViewController.swift | 4 + .../Registration/OnboardingController.swift | 153 +++++++++++++++++- ...OnboardingVerificationViewController.swift | 22 ++- .../src/Account/TSAccountManager.h | 2 + 4 files changed, 179 insertions(+), 2 deletions(-) diff --git a/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift index ac9125a27..79bffb0c2 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift @@ -82,6 +82,8 @@ public class OnboardingBaseViewController: OWSViewController { super.viewWillAppear(animated) self.navigationController?.isNavigationBarHidden = true + // Disable "back" gesture. + self.navigationController?.navigationItem.backBarButtonItem?.isEnabled = false // TODO: Is there a better way to do this? if let navigationController = self.navigationController as? OWSNavigationController { @@ -95,6 +97,8 @@ public class OnboardingBaseViewController: OWSViewController { super.viewDidAppear(animated) self.navigationController?.isNavigationBarHidden = true + // Disable "back" gesture. + self.navigationController?.navigationItem.backBarButtonItem?.isEnabled = false } // MARK: - Orientation diff --git a/Signal/src/ViewControllers/Registration/OnboardingController.swift b/Signal/src/ViewControllers/Registration/OnboardingController.swift index 5909cd674..e500f393f 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingController.swift @@ -66,6 +66,14 @@ public class OnboardingController: NSObject { return TSAccountManager.sharedInstance() } + private var accountManager: AccountManager { + return AppEnvironment.shared.accountManager + } + + private var backup: OWSBackup { + return AppEnvironment.shared.backup + } + // MARK: - @objc @@ -148,6 +156,97 @@ public class OnboardingController: NSObject { navigationController.pushViewController(view, animated: true) } + @objc + public func verificationDidComplete(fromView view: UIViewController) { + AssertIsOnMainThread() + + Logger.info("") + + if tsAccountManager.isReregistering() { + showProfileView(fromView: view) + } else { + checkCanImportBackup(fromView: view) + } + } + + private func showProfileView(fromView view: UIViewController) { + AssertIsOnMainThread() + + Logger.info("") + + guard let navigationController = view.navigationController else { + owsFailDebug("Missing navigationController") + return + } + + ProfileViewController.present(forRegistration: navigationController) + } + + private func showBackupRestoreView(fromView view: UIViewController) { + AssertIsOnMainThread() + + Logger.info("") + + guard let navigationController = view.navigationController else { + owsFailDebug("Missing navigationController") + return + } + + let restoreView = BackupRestoreViewController() + navigationController.setViewControllers([restoreView], animated: true) + } + + private func checkCanImportBackup(fromView view: UIViewController) { + AssertIsOnMainThread() + + Logger.info("") + + self.backup.checkCanImport({ (canImport) in + Logger.info("canImport: \(canImport)") + + if (canImport) { + self.backup.setHasPendingRestoreDecision(true) + + self.showBackupRestoreView(fromView: view) + } else { + self.showProfileView(fromView: view) + } + }) { (_) in + self.showBackupCheckFailedAlert(fromView: view) + } + } + + private func showBackupCheckFailedAlert(fromView view: UIViewController) { + AssertIsOnMainThread() + + Logger.info("") + + let alert = UIAlertController(title: NSLocalizedString("CHECK_FOR_BACKUP_FAILED_TITLE", + comment: "Title for alert shown when the app failed to check for an existing backup."), + message: NSLocalizedString("CHECK_FOR_BACKUP_FAILED_MESSAGE", + comment: "Message for alert shown when the app failed to check for an existing backup."), + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("REGISTER_FAILED_TRY_AGAIN", comment: ""), + style: .default) { (_) in + self.checkCanImportBackup(fromView: view) + }) + alert.addAction(UIAlertAction(title: NSLocalizedString("CHECK_FOR_BACKUP_DO_NOT_RESTORE", comment: "The label for the 'do not restore backup' button."), + style: .destructive) { (_) in + self.showProfileView(fromView: view) + }) + view.present(alert, animated: true) + } + + public func onboardingDidRequire2FAPin(viewController: UIViewController) { + AssertIsOnMainThread() + + Logger.info("") + + // TODO: +// let view = OnboardingCaptchaViewController(onboardingController: self) +// navigationController.pushViewController(view, animated: true) + } + // MARK: - State public private(set) var countryState: OnboardingCountryState = .defaultValue @@ -276,7 +375,6 @@ public class OnboardingController: NSObject { Logger.info("Captcha requested.") onboardingDidRequireCaptcha(viewController: viewController) - return } else if error.code == 400 { OWSAlerts.showAlert(title: NSLocalizedString("REGISTRATION_ERROR", comment: ""), message: NSLocalizedString("REGISTRATION_NON_VALID_NUMBER", comment: "")) @@ -286,4 +384,57 @@ public class OnboardingController: NSObject { message: error.localizedRecoverySuggestion) } } + + // MARK: - Verification + + public func tryToVerify(fromViewController: UIViewController, + verificationCode: String, + pin: String?) { + AssertIsOnMainThread() + + guard let phoneNumber = phoneNumber else { + owsFailDebug("Missing phoneNumber.") + return + } + + // Ensure the account manager state is up-to-date. + // + // TODO: We could skip this in production. + tsAccountManager.phoneNumberAwaitingVerification = phoneNumber.e164 + + ModalActivityIndicatorViewController.present(fromViewController: fromViewController, + canCancel: true) { (modal) in + + self.accountManager.register(verificationCode: verificationCode, pin: pin) + .done { (_) in + DispatchQueue.main.async { + modal.dismiss(completion: { + self.verificationDidComplete(fromView: fromViewController) + }) + } + }.catch({ (error) in + Logger.error("Error: \(error)") + + DispatchQueue.main.async { + modal.dismiss(completion: { + self.verificationFailed(fromViewController: fromViewController, error: error as NSError) + }) + } + }).retainUntilComplete() + } + } + + private func verificationFailed(fromViewController: UIViewController, error: NSError) { + if error.domain == OWSSignalServiceKitErrorDomain && + error.code == OWSErrorCode.registrationMissing2FAPIN.rawValue { + + Logger.info("Missing 2FA PIN.") + + onboardingDidRequire2FAPin(viewController: fromViewController) + } else { + OWSAlerts.showAlert(title: NSLocalizedString("REGISTRATION_VERIFICATION_FAILED_TITLE", comment: "Alert view title"), + message: error.localizedDescription, + fromViewController: fromViewController) + } + } } diff --git a/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift index 31c7f1b36..471e000f9 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift @@ -85,6 +85,9 @@ private class OnboardingCodeView: UIView { var isComplete: Bool { return digitText.count == digitCount } + var verificationCode: String { + return digitText + } private func createSubviews() { textfield.textAlignment = .left @@ -203,11 +206,15 @@ extension OnboardingCodeView: UITextFieldDelegate { updateViewState() + self.delegate?.codeViewDidChange() + // Inform our caller that we took care of performing the change. return false } public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + self.delegate?.codeViewDidChange() + return false } } @@ -258,6 +265,8 @@ public class OnboardingVerificationViewController: OnboardingBaseViewController comment: "Label for the link that lets users change their phone number."), selector: #selector(backLinkTapped)) + onboardingCodeView.delegate = self + let codeStateLink = self.linkButton(title: "", selector: #selector(resendCodeLinkTapped)) codeStateLink.enableMultilineLabel() @@ -450,12 +459,23 @@ public class OnboardingVerificationViewController: OnboardingBaseViewController self.present(actionSheet, animated: true) } + + private func tryToVerify() { + Logger.info("") + + guard onboardingCodeView.isComplete else { + return + } + onboardingController.tryToVerify(fromViewController: self, verificationCode: onboardingCodeView.verificationCode, pin: nil) + } } // MARK: - extension OnboardingVerificationViewController: OnboardingCodeViewDelegate { public func codeViewDidChange() { - // TODO: + AssertIsOnMainThread() + + tryToVerify() } } diff --git a/SignalServiceKit/src/Account/TSAccountManager.h b/SignalServiceKit/src/Account/TSAccountManager.h index a2aff043e..1f0baf7b5 100644 --- a/SignalServiceKit/src/Account/TSAccountManager.h +++ b/SignalServiceKit/src/Account/TSAccountManager.h @@ -32,6 +32,8 @@ typedef NS_ENUM(NSUInteger, OWSRegistrationState) { @property (nonatomic, nullable) NSString *phoneNumberAwaitingVerification; #endif +- (void)setPhoneNumberAwaitingVerification:(NSString *_Nullable)phoneNumberAwaitingVerification; + #pragma mark - Initializers - (instancetype)init NS_UNAVAILABLE; From c2b2d38f24356bd74cde2b01ac2d2e2f10730647 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 15 Feb 2019 14:58:06 -0500 Subject: [PATCH 4/7] Sketch out the 'onboarding code verification' view. --- .../ViewControllers/HomeView/HomeViewController.m | 13 ------------- .../Registration/OnboardingBaseViewController.swift | 4 ---- Signal/translations/en.lproj/Localizable.strings | 3 --- 3 files changed, 20 deletions(-) diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m index 88eb9f6f0..2e8ab8ba1 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -482,19 +482,6 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations [self.searchResultsController viewDidAppear:animated]; self.hasEverAppeared = YES; - - dispatch_async(dispatch_get_main_queue(), ^{ - OnboardingController *onboardingController = [OnboardingController new]; - [onboardingController - updateWithPhoneNumber:[[OnboardingPhoneNumber alloc] initWithE164:@"+13213214321" userInput:@"3213214321"]]; - - // UIViewController *view = [onboardingController initialViewController]; - UIViewController *view = - [[OnboardingVerificationViewController alloc] initWithOnboardingController:onboardingController]; - OWSNavigationController *navigationController = - [[OWSNavigationController alloc] initWithRootViewController:view]; - [self presentViewController:navigationController animated:YES completion:nil]; - }); } - (void)viewDidDisappear:(BOOL)animated diff --git a/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift index 79bffb0c2..7ed43e10a 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift @@ -57,10 +57,6 @@ public class OnboardingBaseViewController: OWSViewController { return button(title: title, selector: selector, titleColor: .ows_materialBlue, backgroundColor: .white) } - func disabledLinkButton(title: String, selector: Selector) -> OWSFlatButton { - return self.button(title: title, selector: selector, titleColor: Theme.secondaryColor, backgroundColor: .white) - } - private func button(title: String, selector: Selector, titleColor: UIColor, backgroundColor: UIColor) -> OWSFlatButton { // TODO: Make sure this all fits if dynamic font sizes are maxed out. let font = UIFont.ows_dynamicTypeBodyClamped.ows_mediumWeight() diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 06695486d..a8bce1482 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1556,9 +1556,6 @@ /* 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?"; From 854a75ae65b53aa302734cf414858d14ad74c463 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 15 Feb 2019 14:59:46 -0500 Subject: [PATCH 5/7] Sketch out the 'onboarding code verification' view. --- .../Registration/OnboardingVerificationViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift index 471e000f9..eb322987c 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift @@ -277,7 +277,7 @@ public class OnboardingVerificationViewController: OnboardingBaseViewController let stackView = UIStackView(arrangedSubviews: [ titleLabel, - UIView.spacer(withHeight: 21), + UIView.spacer(withHeight: 12), backLink, topSpacer, onboardingCodeView, From b4aec587956fffa966936471d5f9b21a24322ec0 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 15 Feb 2019 15:03:20 -0500 Subject: [PATCH 6/7] Sketch out the 'onboarding code verification' view. --- .../Registration/OnboardingVerificationViewController.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift index eb322987c..f5e64af43 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift @@ -420,10 +420,6 @@ public class OnboardingVerificationViewController: OnboardingBaseViewController self.navigationController?.popViewController(animated: true) } - @objc func ignoreEvent() { - Logger.info("") - } - @objc func resendCodeLinkTapped() { Logger.info("") From e1dc534fe69cbb40ae2f6dfe198942d45f7365dc Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 18 Feb 2019 09:52:09 -0500 Subject: [PATCH 7/7] Respond to CR. --- .../Registration/OnboardingController.swift | 6 ++-- ...OnboardingVerificationViewController.swift | 35 +++++++++---------- SignalServiceKit/src/Util/String+SSK.swift | 8 +---- 3 files changed, 20 insertions(+), 29 deletions(-) diff --git a/Signal/src/ViewControllers/Registration/OnboardingController.swift b/Signal/src/ViewControllers/Registration/OnboardingController.swift index e500f393f..000cce21f 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingController.swift @@ -201,7 +201,7 @@ public class OnboardingController: NSObject { Logger.info("") - self.backup.checkCanImport({ (canImport) in + backup.checkCanImport({ (canImport) in Logger.info("canImport: \(canImport)") if (canImport) { @@ -211,9 +211,9 @@ public class OnboardingController: NSObject { } else { self.showProfileView(fromView: view) } - }) { (_) in + }, failure: { (_) in self.showBackupCheckFailedAlert(fromView: view) - } + }) } private func showBackupCheckFailedAlert(fromView view: UIViewController) { diff --git a/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift index f5e64af43..751aa2d8b 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingVerificationViewController.swift @@ -152,7 +152,7 @@ private class OnboardingCodeView: UIView { guard index < digitText.count else { return "" } - return digitText.substring(from: index).trim(after: 1) + return digitText.substring(from: index).substring(to: 1) } // Ensure that all labels are displaying the correct @@ -199,10 +199,10 @@ extension OnboardingCodeView: UITextFieldDelegate { let unfiltered = left + newString + right let characterSet = CharacterSet(charactersIn: "0123456789") let filtered = unfiltered.components(separatedBy: characterSet.inverted).joined() - let filteredAndTrimmed = filtered.trim(after: 1) + let filteredAndTrimmed = filtered.substring(to: 1) textField.text = filteredAndTrimmed - digitText = digitText.trim(after: currentDigitIndex) + filteredAndTrimmed + digitText = digitText.substring(to: currentDigitIndex) + filteredAndTrimmed updateViewState() @@ -238,14 +238,14 @@ extension OnboardingCodeView: OnboardingCodeViewTextFieldDelegate { public class OnboardingVerificationViewController: OnboardingBaseViewController { private enum CodeState { - case pending - case possiblyNotDelivered + case sent + case readyForResend case resent } // MARK: - - private var codeState = CodeState.pending + private var codeState = CodeState.sent private var titleLabel: UILabel? private let phoneNumberTextField = UITextField() @@ -308,14 +308,12 @@ public class OnboardingVerificationViewController: OnboardingBaseViewController private var codeCountdownStart: NSDate? deinit { - if let codeCountdownTimer = codeCountdownTimer { - codeCountdownTimer.invalidate() - } + codeCountdownTimer?.invalidate() } private func startCodeCountdown() { codeCountdownStart = NSDate() - codeCountdownTimer = Timer.weakScheduledTimer(withTimeInterval: 1, target: self, selector: #selector(codeCountdownTimerFired), userInfo: nil, repeats: true) + codeCountdownTimer = Timer.weakScheduledTimer(withTimeInterval: 0.25, target: self, selector: #selector(codeCountdownTimerFired), userInfo: nil, repeats: true) } @objc @@ -336,10 +334,10 @@ public class OnboardingVerificationViewController: OnboardingBaseViewController codeCountdownTimer.invalidate() self.codeCountdownTimer = nil - if codeState != .pending { + if codeState != .sent { owsFailDebug("Unexpected codeState: \(codeState)") } - codeState = .possiblyNotDelivered + codeState = .readyForResend updateCodeState() return } @@ -372,7 +370,7 @@ public class OnboardingVerificationViewController: OnboardingBaseViewController // Update titleLabel switch codeState { - case .pending, .possiblyNotDelivered: + case .sent, .readyForResend: 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) @@ -384,16 +382,15 @@ public class OnboardingVerificationViewController: OnboardingBaseViewController // Update codeStateLink switch codeState { - case .pending: + case .sent: 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}}."), + comment: "Format for the label of the 'sent 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: + case .readyForResend: 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, @@ -424,10 +421,10 @@ public class OnboardingVerificationViewController: OnboardingBaseViewController Logger.info("") switch codeState { - case .pending: + case .sent: // Ignore taps until the countdown expires. break - case .possiblyNotDelivered, .resent: + case .readyForResend, .resent: showResendActionSheet() } } diff --git a/SignalServiceKit/src/Util/String+SSK.swift b/SignalServiceKit/src/Util/String+SSK.swift index 6312dd677..3b6684c5e 100644 --- a/SignalServiceKit/src/Util/String+SSK.swift +++ b/SignalServiceKit/src/Util/String+SSK.swift @@ -18,12 +18,6 @@ public extension String { } public func substring(to index: Int) -> String { - return String(self[.. String { - let index = min(maxCount, self.count) - return substring(to: index) + return String(prefix(index)) } }