diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 83685fcda..8d157d008 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -76,6 +76,7 @@ 3448E15E221333F5004B052E /* OnboardingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448E15D221333F5004B052E /* OnboardingController.swift */; }; 3448E16022134C89004B052E /* OnboardingSplashViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448E15F22134C88004B052E /* OnboardingSplashViewController.swift */; }; 3448E1622213585C004B052E /* OnboardingBaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448E1612213585C004B052E /* OnboardingBaseViewController.swift */; }; + 3448E16422135FFA004B052E /* OnboardingPhoneNumberViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448E16322135FFA004B052E /* OnboardingPhoneNumberViewController.swift */; }; 344F248D2007CCD600CFB4F4 /* DisplayableText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344F248C2007CCD600CFB4F4 /* DisplayableText.swift */; }; 345BC30C2047030700257B7C /* OWS2FASettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 345BC30B2047030600257B7C /* OWS2FASettingsViewController.m */; }; 3461284B1FD0B94000532771 /* SAELoadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3461284A1FD0B93F00532771 /* SAELoadViewController.swift */; }; @@ -739,6 +740,7 @@ 3448E15D221333F5004B052E /* OnboardingController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingController.swift; sourceTree = ""; }; 3448E15F22134C88004B052E /* OnboardingSplashViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingSplashViewController.swift; sourceTree = ""; }; 3448E1612213585C004B052E /* OnboardingBaseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingBaseViewController.swift; sourceTree = ""; }; + 3448E16322135FFA004B052E /* OnboardingPhoneNumberViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingPhoneNumberViewController.swift; sourceTree = ""; }; 34491FC11FB0F78500B3E5A3 /* my */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = my; path = translations/my.lproj/Localizable.strings; sourceTree = ""; }; 344F248C2007CCD600CFB4F4 /* DisplayableText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayableText.swift; sourceTree = ""; }; 345BC30A2047030600257B7C /* OWS2FASettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWS2FASettingsViewController.h; sourceTree = ""; }; @@ -1466,6 +1468,7 @@ 3448E1612213585C004B052E /* OnboardingBaseViewController.swift */, 3448E15D221333F5004B052E /* OnboardingController.swift */, 3448E15B22133274004B052E /* OnboardingPermissionsViewController.swift */, + 3448E16322135FFA004B052E /* OnboardingPhoneNumberViewController.swift */, 3448E15F22134C88004B052E /* OnboardingSplashViewController.swift */, 346E9D5321B040B600562252 /* RegistrationController.swift */, 340FC878204DAC8C007AEB0F /* RegistrationViewController.h */, @@ -3542,6 +3545,7 @@ 4556FA681F54AA9500AF40DD /* DebugUIProfile.swift in Sources */, 45A6DAD61EBBF85500893231 /* ReminderView.swift in Sources */, 34D1F0881F8678AA0066283D /* ConversationViewLayout.m in Sources */, + 3448E16422135FFA004B052E /* OnboardingPhoneNumberViewController.swift in Sources */, 34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */, 344825C6211390C800DB4BD8 /* OWSOrphanDataCleaner.m in Sources */, 45D2AC02204885170033C692 /* OWS2FAReminderViewController.swift in Sources */, diff --git a/Signal/src/Models/AccountManager.swift b/Signal/src/Models/AccountManager.swift index eb7d36cad..1b2d6ee5e 100644 --- a/Signal/src/Models/AccountManager.swift +++ b/Signal/src/Models/AccountManager.swift @@ -19,17 +19,6 @@ public class AccountManager: NSObject { return OWSProfileManager.shared() } - // MARK: - - - @objc - public override init() { - super.init() - - SwiftSingletons.register(self) - } - - // MARK: - Dependencies - private var networkManager: TSNetworkManager { return SSKEnvironment.shared.networkManager } @@ -42,6 +31,15 @@ public class AccountManager: NSObject { return TSAccountManager.sharedInstance() } + // MARK: - + + @objc + public override init() { + super.init() + + SwiftSingletons.register(self) + } + // MARK: registration @objc func registerObjc(verificationCode: String, diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m index 2e8ab8ba1..ff5d49af5 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -482,6 +482,17 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations [self.searchResultsController viewDidAppear:animated]; self.hasEverAppeared = YES; + + dispatch_async(dispatch_get_main_queue(), ^{ + id onboardingController = [OnboardingControllerImpl new]; + OnboardingPhoneNumberViewController *view = + [[OnboardingPhoneNumberViewController alloc] initWithOnboardingController:onboardingController]; + // OnboardingPermissionsViewController *view = + // [[OnboardingPermissionsViewController 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 9f6546b09..52cc3ff28 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift @@ -15,6 +15,8 @@ public class OnboardingBaseViewController: OWSViewController { self.onboardingController = onboardingController super.init(nibName: nil, bundle: nil) + + self.shouldUseTheme = false } @available(*, unavailable, message: "use other init() instead.") @@ -54,7 +56,7 @@ public class OnboardingBaseViewController: OWSViewController { return explanationLabel } - func button(title: String, selector: Selector) -> UIView { + func button(title: String, selector: Selector) -> OWSFlatButton { // TODO: Make sure this all fits if dynamic font sizes are maxed out. let buttonHeight: CGFloat = 48 let button = OWSFlatButton.button(title: title, @@ -72,4 +74,8 @@ public class OnboardingBaseViewController: OWSViewController { public override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .portrait } + + public override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } } diff --git a/Signal/src/ViewControllers/Registration/OnboardingController.swift b/Signal/src/ViewControllers/Registration/OnboardingController.swift index 7514dc8b3..fca6a964d 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingController.swift @@ -12,6 +12,8 @@ public protocol OnboardingController: class { func onboardingPermissionsWasSkipped(viewController: UIViewController) func onboardingPermissionsDidComplete(viewController: UIViewController) + + func onboardingPhoneNumberDidComplete(viewController: UIViewController) } // MARK: - @@ -28,7 +30,21 @@ public class OnboardingControllerImpl: NSObject, OnboardingController { viewController.navigationController?.pushViewController(view, animated: true) } - public func onboardingPermissionsWasSkipped(viewController: UIViewController) {} + public func onboardingPermissionsWasSkipped(viewController: UIViewController) { + pushPhoneNumberView(viewController: viewController) + } - public func onboardingPermissionsDidComplete(viewController: UIViewController) {} + public func onboardingPermissionsDidComplete(viewController: UIViewController) { + pushPhoneNumberView(viewController: viewController) + } + + private func pushPhoneNumberView(viewController: UIViewController) { + let view = OnboardingPhoneNumberViewController(onboardingController: self) + viewController.navigationController?.pushViewController(view, animated: true) + } + + public func onboardingPhoneNumberDidComplete(viewController: UIViewController) { + // CodeVerificationViewController *vc = [CodeVerificationViewController new]; + // [weakSelf.navigationController pushViewController:vc animated:YES]; + } } diff --git a/Signal/src/ViewControllers/Registration/OnboardingPhoneNumberViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingPhoneNumberViewController.swift new file mode 100644 index 000000000..e12e1c8ec --- /dev/null +++ b/Signal/src/ViewControllers/Registration/OnboardingPhoneNumberViewController.swift @@ -0,0 +1,514 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import UIKit +import PromiseKit + +@objc +public class OnboardingPhoneNumberViewController: OnboardingBaseViewController { + + // MARK: - Dependencies + + private var tsAccountManager: TSAccountManager { + return TSAccountManager.sharedInstance() + } + + // MARK: - + + private let countryNameLabel = UILabel() + private let callingCodeLabel = UILabel() + private let phoneNumberTextField = UITextField() + private var nextButton: OWSFlatButton? + + // - (void)didTapLegalTerms:(UIButton *)sender +//{ +// [[UIApplication sharedApplication] openURL:[NSURL URLWithString:kLegalTermsUrlString]]; +// } +//#pragma mark - Keyboard notifications +// +//- (void)initializeKeyboardHandlers +//{ +// UITapGestureRecognizer *outsideTabRecognizer = +// [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissKeyboardFromAppropriateSubView)]; +// [self.view addGestureRecognizer:outsideTabRecognizer]; +// } +// +// - (void)dismissKeyboardFromAppropriateSubView +// { +// [self.view endEditing:NO]; +//} +// + + override public func loadView() { + super.loadView() + + // TODO: Is this still necessary? + if let navigationController = self.navigationController as? OWSNavigationController { + SignalApp.shared().signUpFlowNavigationController = navigationController + } else { + owsFailDebug("Missing or invalid navigationController") + } + + populateDefaults() + + view.backgroundColor = Theme.backgroundColor + view.layoutMargins = .zero + + // TODO: +// navigationItem.title = NSLocalizedString("SETTINGS_BACKUP", comment: "Label for the backup view in app settings.") + + let titleLabel = self.titleLabel(text: NSLocalizedString("ONBOARDING_PHONE_NUMBER_TITLE", comment: "Title of the 'onboarding phone number' view.")) + + // Country + + // TODO: dynamic + let fontSizePoints: CGFloat = ScaleFromIPhone5To7Plus(16, 20) + let rowHeight: CGFloat = 40 + + countryNameLabel.textColor = Theme.primaryColor + countryNameLabel.font = UIFont.ows_dynamicTypeBody + countryNameLabel.setContentHuggingHorizontalLow() + countryNameLabel.setCompressionResistanceHorizontalLow() + + let countryIcon = UIImage(named: (CurrentAppContext().isRTL + ? "small_chevron_left" + : "small_chevron_right")) +// NavBarBackRTL +// small_chevron_right +// system_disclosure_indicator_rtl + let countryImageView = UIImageView(image: countryIcon?.withRenderingMode(.alwaysTemplate)) + countryImageView.tintColor = Theme.placeholderColor + countryImageView.setContentHuggingHigh() + countryImageView.setCompressionResistanceHigh() + + let countryRow = UIStackView(arrangedSubviews: [ + countryNameLabel, + countryImageView + ]) + countryRow.axis = .horizontal + countryRow.alignment = .center + countryRow.spacing = 10 + countryRow.isUserInteractionEnabled = true + countryRow.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(countryRowTapped))) + countryRow.autoSetDimension(.height, toSize: rowHeight) + addBottomStroke(countryRow) + + callingCodeLabel.textColor = Theme.primaryColor + callingCodeLabel.font = UIFont.ows_dynamicTypeBody + callingCodeLabel.setContentHuggingHorizontalHigh() + callingCodeLabel.setCompressionResistanceHorizontalHigh() + callingCodeLabel.isUserInteractionEnabled = true + callingCodeLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(countryCodeTapped))) + addBottomStroke(callingCodeLabel) + callingCodeLabel.autoSetDimension(.width, toSize: rowHeight, relation: .greaterThanOrEqual) + + phoneNumberTextField.textAlignment = .left + phoneNumberTextField.delegate = self + phoneNumberTextField.keyboardType = .numberPad + phoneNumberTextField.textColor = Theme.primaryColor + phoneNumberTextField.font = UIFont.ows_dynamicTypeBody + phoneNumberTextField.setContentHuggingHorizontalLow() + phoneNumberTextField.setCompressionResistanceHorizontalLow() + + addBottomStroke(phoneNumberTextField) + + let phoneNumberRow = UIStackView(arrangedSubviews: [ + callingCodeLabel, + phoneNumberTextField + ]) + phoneNumberRow.axis = .horizontal + phoneNumberRow.alignment = .fill + phoneNumberRow.spacing = 10 + phoneNumberRow.autoSetDimension(.height, toSize: rowHeight) + callingCodeLabel.autoMatch(.height, to: .height, of: phoneNumberTextField) + + // 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, + topSpacer, + countryRow, + UIView.spacer(withHeight: 8), + phoneNumberRow, + bottomSpacer, + nextButton + ]) + stackView.axis = .vertical + stackView.alignment = .fill + stackView.layoutMargins = UIEdgeInsets(top: 32, left: 32, bottom: 32, right: 32) + stackView.isLayoutMarginsRelativeArrangement = true + view.addSubview(stackView) + stackView.autoPinWidthToSuperviewMargins() + stackView.autoPinWidthToSuperviewMargins() + 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 viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.navigationController?.isNavigationBarHidden = false + + phoneNumberTextField.becomeFirstResponder() + } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.navigationController?.isNavigationBarHidden = false + + 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 callingCode = parsedPhoneNumber.getCountryCode() else { + owsFailDebug("Could not resume re-registration; missing callingCode.") + return + } + let callingCodeText = "\(COUNTRY_CODE_PREFIX)\(callingCode)" + let countryCodes: [String] = + PhoneNumberUtil.sharedThreadLocal().countryCodes(fromCallingCode: callingCodeText) + 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(callingCodeText) { + owsFailDebug("Could not resume re-registration; non-matching calling code.") + return + } + let phoneNumberWithoutCallingCode = phoneNumberE164.substring(from: callingCodeText.count) + + update(withCountryName: countryName, callingCode: callingCodeText, countryCode: countryCode) + + phoneNumberTextField.text = phoneNumberWithoutCallingCode + // Don't let user edit their phone number while re-registering. + phoneNumberTextField.isEnabled = false + } + + // MARK: - + + private var countryName = "" + private var callingCode = "" + private var countryCode = "" + + private func populateDefaults() { + + var countryCode: String = PhoneNumber.defaultCountryCode() + if let lastRegisteredCountryCode = self.lastRegisteredCountryCode(), + lastRegisteredCountryCode.count > 0 { + countryCode = lastRegisteredCountryCode + } + + let callingCodeNumber: NSNumber = PhoneNumberUtil.sharedThreadLocal().nbPhoneNumberUtil.getCountryCode(forRegion: countryCode) + let callingCode = "\(COUNTRY_CODE_PREFIX)\(callingCodeNumber)" + + if let lastRegisteredPhoneNumber = self.lastRegisteredPhoneNumber(), + lastRegisteredPhoneNumber.count > 0, + lastRegisteredPhoneNumber.hasPrefix(callingCode) { + phoneNumberTextField.text = lastRegisteredPhoneNumber.substring(from: callingCode.count) + } + + var countryName = NSLocalizedString("UNKNOWN_COUNTRY_NAME", comment: "Label for unknown countries.") + if let countryNameDerived = PhoneNumberUtil.countryName(fromCountryCode: countryCode) { + countryName = countryNameDerived + } + + update(withCountryName: countryName, callingCode: callingCode, countryCode: countryCode) + } + + private func update(withCountryName countryName: String, callingCode: String, countryCode: String) { + AssertIsOnMainThread() + + 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 + } + + self.countryName = countryName + self.callingCode = callingCode + self.countryCode = countryCode + + countryNameLabel.text = countryName + callingCodeLabel.text = callingCode + + self.phoneNumberTextField.placeholder = ViewControllerUtils.examplePhoneNumber(forCountryCode: countryCode, callingCode: callingCode) + } + + // MARK: - Debug + + private let kKeychainService_LastRegistered = "kKeychainService_LastRegistered" + private let kKeychainKey_LastRegisteredCountryCode = "kKeychainKey_LastRegisteredCountryCode" + private let kKeychainKey_LastRegisteredPhoneNumber = "kKeychainKey_LastRegisteredPhoneNumber" + + private func debugValue(forKey key: String) -> String? { + guard CurrentAppContext().isDebugBuild() else { + return nil + } + + do { + let value = try CurrentAppContext().keychainStorage().string(forService: kKeychainService_LastRegistered, key: key) + return value + } catch { + owsFailDebug("Error: \(error)") + return nil + } + } + + private func setDebugValue(_ value: String, forKey key: String) { + guard CurrentAppContext().isDebugBuild() else { + return + } + + do { + try CurrentAppContext().keychainStorage().set(string: value, service: kKeychainService_LastRegistered, key: key) + } catch { + owsFailDebug("Error: \(error)") + } + } + + private func lastRegisteredCountryCode() -> String? { + return debugValue(forKey: kKeychainKey_LastRegisteredCountryCode) + } + + private func setLastRegisteredCountryCode(value: String) { + setDebugValue(value, forKey: kKeychainKey_LastRegisteredCountryCode) + } + + private func lastRegisteredPhoneNumber() -> String? { + return debugValue(forKey: kKeychainKey_LastRegisteredPhoneNumber) + } + + private func setLastRegisteredPhoneNumber(value: String) { + setDebugValue(value, forKey: kKeychainKey_LastRegisteredPhoneNumber) + } + + // MARK: - Events + + @objc func explanationLabelTapped(sender: UIGestureRecognizer) { + guard sender.state == .recognized else { + return + } + // TODO: + } + + @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("") + + onboardingController.onboardingPhoneNumberDidComplete(viewController: self) + } + + // 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 didTapRegisterButton() { + guard let phoneNumberText = phoneNumberTextField.text?.ows_stripped(), + phoneNumberText.count > 0 else { + OWSAlerts.showAlert(title: + NSLocalizedString("REGISTRATION_VIEW_NO_PHONE_NUMBER_ALERT_TITLE", + comment: "Title of alert indicating that users needs to enter a phone number to register."), + message: + NSLocalizedString("REGISTRATION_VIEW_NO_PHONE_NUMBER_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_PHONE_NUMBER_ALERT_TITLE", + comment: "Title of alert indicating that users needs to enter a valid phone number to register."), + message: + NSLocalizedString("REGISTRATION_VIEW_INVALID_PHONE_NUMBER_ALERT_MESSAGE", + comment: "Message of alert indicating that users needs to enter a valid phone number to register.")) + return + } + let parsedPhoneNumber = localNumber.toE164() + + if UIDevice.current.isIPad { + let countryCode = self.countryCode + 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.sendCode(parsedPhoneNumber: parsedPhoneNumber, + phoneNumberText: phoneNumberText, + countryCode: countryCode) + }) + } else { + sendCode(parsedPhoneNumber: parsedPhoneNumber, + phoneNumberText: phoneNumberText, + countryCode: countryCode) + } + } + + private func sendCode(parsedPhoneNumber: String, + phoneNumberText: String, + countryCode: String) { + ModalActivityIndicatorViewController.present(fromViewController: self, + canCancel: true) { (modal) in + self.setLastRegisteredCountryCode(value: countryCode) + self.setLastRegisteredPhoneNumber(value: phoneNumberText) + + self.tsAccountManager.register(withPhoneNumber: parsedPhoneNumber, + success: { + DispatchQueue.main.async { + modal.dismiss(completion: { + self.registrationSucceeded() + }) + } + }, failure: { (error) in + Logger.error("Error: \(error)") + + DispatchQueue.main.async { + modal.dismiss(completion: { + self.registrationFailed(error: error as NSError) + }) + } + }, smsVerification: true) + } + } + + private func registrationSucceeded() { + self.onboardingController.onboardingPhoneNumberDidComplete(viewController: self) + } + + private func registrationFailed(error: NSError) { + if error.code == 400 { + OWSAlerts.showAlert(title: NSLocalizedString("REGISTRATION_ERROR", comment: ""), + message: NSLocalizedString("REGISTRATION_NON_VALID_NUMBER", comment: "")) + + } else { + OWSAlerts.showAlert(title: error.localizedDescription, + message: error.localizedRecoverySuggestion) + } + + phoneNumberTextField.becomeFirstResponder() + } +} + +// MARK: - + +extension OnboardingPhoneNumberViewController: UITextFieldDelegate { + public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + var prefix = self.callingCode + if prefix.hasPrefix(COUNTRY_CODE_PREFIX) { + prefix = prefix.substring(from: COUNTRY_CODE_PREFIX.count) + } + ViewControllerUtils.phoneNumber(textField, shouldChangeCharactersIn: range, replacementString: string, countryCode: countryCode, prefix: prefix) + + // Inform our caller that we took care of performing the change. + return false + } + + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + didTapRegisterButton() + textField.resignFirstResponder() + return false + } +} + +// MARK: - + +extension OnboardingPhoneNumberViewController: 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 + } + + update(withCountryName: countryName, callingCode: callingCode, countryCode: countryCode) + + // Trigger the formatting logic with a no-op edit. + _ = textField(phoneNumberTextField, shouldChangeCharactersIn: NSRange(location: 0, length: 0), replacementString: "") + } +} diff --git a/Signal/src/util/MainAppContext.m b/Signal/src/util/MainAppContext.m index 8631e9291..f3cf57c97 100644 --- a/Signal/src/util/MainAppContext.m +++ b/Signal/src/util/MainAppContext.m @@ -314,6 +314,15 @@ NSString *const ReportedApplicationStateDidChangeNotification = @"ReportedApplic return [[NSUserDefaults alloc] initWithSuiteName:SignalApplicationGroup]; } +- (BOOL)isDebugBuild +{ +#ifdef DEBUG + return YES; +#else + return NO; +#endif +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 1d161d876..dfcb3443b 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -302,6 +302,9 @@ /* Label for generic done button. */ "BUTTON_DONE" = "Done"; +/* Label for the 'next' button. */ +"BUTTON_NEXT" = "Next"; + /* Label for redo button. */ "BUTTON_REDO" = "Redo"; @@ -1520,6 +1523,9 @@ /* Title of the 'onboarding permissions' view. */ "ONBOARDING_PERMISSIONS_TITLE" = "We need access to your contacts and notifications"; +/* Title of the 'onboarding phone number' view. */ +"ONBOARDING_PHONE_NUMBER_TITLE" = "Enter your phone number to get started"; + /* Explanation in the 'onboarding splash' view. */ "ONBOARDING_SPLASH_EXPLANATION" = "By continuing, you agree to Signal's terms."; @@ -2354,6 +2360,9 @@ /* Displayed if for some reason we can't determine a contacts phone number *or* name */ "UNKNOWN_CONTACT_NAME" = "Unknown Contact"; +/* Label for unknown countries. */ +"UNKNOWN_COUNTRY_NAME" = "Unknown Country"; + /* Indicates an unknown or unrecognizable value. */ "UNKNOWN_VALUE" = "Unknown"; diff --git a/SignalMessaging/ViewControllers/CountryCodeViewController.h b/SignalMessaging/ViewControllers/CountryCodeViewController.h index deb98c044..7f9a70352 100644 --- a/SignalMessaging/ViewControllers/CountryCodeViewController.h +++ b/SignalMessaging/ViewControllers/CountryCodeViewController.h @@ -4,6 +4,8 @@ #import "OWSTableViewController.h" +NS_ASSUME_NONNULL_BEGIN + @class CountryCodeViewController; @protocol CountryCodeViewControllerDelegate @@ -26,3 +28,5 @@ @property (nonatomic) UIInterfaceOrientationMask interfaceOrientationMask; @end + +NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/ViewControllers/CountryCodeViewController.m b/SignalMessaging/ViewControllers/CountryCodeViewController.m index 16ea98bc4..264c2d316 100644 --- a/SignalMessaging/ViewControllers/CountryCodeViewController.m +++ b/SignalMessaging/ViewControllers/CountryCodeViewController.m @@ -11,6 +11,8 @@ #import "UIView+OWS.h" #import +NS_ASSUME_NONNULL_BEGIN + @interface CountryCodeViewController () @property (nonatomic, readonly) UISearchBar *searchBar; @@ -170,3 +172,5 @@ } @end + +NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/ViewControllers/ViewControllerUtils.h b/SignalMessaging/ViewControllers/ViewControllerUtils.h index bedba7671..c115c3769 100644 --- a/SignalMessaging/ViewControllers/ViewControllerUtils.h +++ b/SignalMessaging/ViewControllers/ViewControllerUtils.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import @@ -22,6 +22,13 @@ extern NSString *const TappedStatusBarNotification; replacementString:(NSString *)insertionText countryCode:(NSString *)countryCode; +// The same method, but it temporarily adds a prefix during the formatting process. ++ (void)phoneNumberTextField:(UITextField *)textField + shouldChangeCharactersInRange:(NSRange)range + replacementString:(NSString *)insertionText + countryCode:(NSString *)countryCode + prefix:(nullable NSString *)prefix; + + (void)ows2FAPINTextField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)insertionText; diff --git a/SignalMessaging/ViewControllers/ViewControllerUtils.m b/SignalMessaging/ViewControllers/ViewControllerUtils.m index c5e5aa457..68ad7fc19 100644 --- a/SignalMessaging/ViewControllers/ViewControllerUtils.m +++ b/SignalMessaging/ViewControllers/ViewControllerUtils.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "ViewControllerUtils.h" @@ -23,7 +23,19 @@ const NSUInteger kMax2FAPinLength = 16; replacementString:(NSString *)insertionText countryCode:(NSString *)countryCode { + return [self phoneNumberTextField:textField + shouldChangeCharactersInRange:range + replacementString:insertionText + countryCode:countryCode + prefix:nil]; +} ++ (void)phoneNumberTextField:(UITextField *)textField + shouldChangeCharactersInRange:(NSRange)range + replacementString:(NSString *)insertionText + countryCode:(NSString *)countryCode + prefix:(nullable NSString *)prefix +{ // Phone numbers takes many forms. // // * We only want to let the user enter decimal digits. @@ -40,6 +52,7 @@ const NSUInteger kMax2FAPinLength = 16; // * Take partial input if possible. NSString *oldText = textField.text; + // Construct the new contents of the text field by: // 1. Determining the "left" substring: the contents of the old text _before_ the deletion range. // Filtering will remove non-decimal digit characters like hyphen "-". @@ -76,6 +89,12 @@ const NSUInteger kMax2FAPinLength = 16; // 5. Construct the "formatted" new text by inserting a hyphen if necessary. // reformat the phone number, trying to keep the cursor beside the inserted or deleted digit NSUInteger cursorPositionAfterChange = MIN(left.length + center.length, textAfterChange.length); + + // if (prefix.length > 0) { + // textAfterChange = [prefix stringByAppendingString:textAfterChange]; + // cursorPositionAfterChange += prefix.length; + // } + NSString *textAfterReformat = [PhoneNumber bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:textAfterChange withSpecifiedCountryCodeString:countryCode]; @@ -83,6 +102,23 @@ const NSUInteger kMax2FAPinLength = 16; from:textAfterChange to:textAfterReformat stickingRightward:isJustDeletion]; + + // if (prefix.length > 0) { + // OWSAssertDebug([textAfterReformat hasPrefix:prefix]); + // if ([textAfterReformat hasPrefix:prefix]) { + // textAfterReformat = [textAfterReformat substringFromIndex:prefix.length]; + // cursorPositionAfterReformat -= prefix.length; + // + // NSRange trimRange = [textAfterReformat + // rangeOfCharacterFromSet:NSCharacterSet.whitespaceCharacterSet.invertedSet]; if (trimRange.location > + // 0) { + // textAfterReformat = [textAfterReformat + // stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet]; + // cursorPositionAfterReformat -= trimRange.location; + // } + // } + // } + textField.text = textAfterReformat; UITextPosition *pos = [textField positionFromPosition:textField.beginningOfDocument offset:(NSInteger)cursorPositionAfterReformat]; diff --git a/SignalMessaging/categories/UIView+OWS.swift b/SignalMessaging/categories/UIView+OWS.swift index 8ca4cc6a9..5082dea68 100644 --- a/SignalMessaging/categories/UIView+OWS.swift +++ b/SignalMessaging/categories/UIView+OWS.swift @@ -87,6 +87,22 @@ extension UIView { return view } + @objc + public class func hStretchingSpacer() -> UIView { + let view = UIView() + view.setContentHuggingHorizontalLow() + view.setCompressionResistanceHorizontalLow() + return view + } + + @objc + public class func vStretchingSpacer() -> UIView { + let view = UIView() + view.setContentHuggingVerticalLow() + view.setCompressionResistanceVerticalLow() + return view + } + @objc public func applyScaleAspectFitLayout(subview: UIView, aspectRatio: CGFloat) -> [NSLayoutConstraint] { guard subviews.contains(subview) else { diff --git a/SignalServiceKit/src/Account/TSAccountManager.h b/SignalServiceKit/src/Account/TSAccountManager.h index dece93ae6..92c873603 100644 --- a/SignalServiceKit/src/Account/TSAccountManager.h +++ b/SignalServiceKit/src/Account/TSAccountManager.h @@ -146,7 +146,7 @@ typedef NS_ENUM(NSUInteger, OWSRegistrationState) { // Returns YES on success. - (BOOL)resetForReregistration; -- (NSString *)reregisterationPhoneNumber; +- (nullable NSString *)reregisterationPhoneNumber; - (BOOL)isReregistering; #pragma mark - Manual Message Fetch diff --git a/SignalServiceKit/src/Account/TSAccountManager.m b/SignalServiceKit/src/Account/TSAccountManager.m index 75261d303..d95b63c53 100644 --- a/SignalServiceKit/src/Account/TSAccountManager.m +++ b/SignalServiceKit/src/Account/TSAccountManager.m @@ -625,7 +625,7 @@ NSString *const TSAccountManager_NeedsAccountAttributesUpdateKey = @"TSAccountMa } } -- (NSString *)reregisterationPhoneNumber +- (nullable NSString *)reregisterationPhoneNumber { OWSAssertDebug([self isReregistering]); diff --git a/SignalServiceKit/src/Contacts/PhoneNumberUtil.h b/SignalServiceKit/src/Contacts/PhoneNumberUtil.h index 416d893b1..377fa7498 100644 --- a/SignalServiceKit/src/Contacts/PhoneNumberUtil.h +++ b/SignalServiceKit/src/Contacts/PhoneNumberUtil.h @@ -18,12 +18,12 @@ NS_ASSUME_NONNULL_BEGIN + (BOOL)name:(NSString *)nameString matchesQuery:(NSString *)queryString; + (NSString *)callingCodeFromCountryCode:(NSString *)countryCode; -+ (NSString *)countryNameFromCountryCode:(NSString *)countryCode; ++ (nullable NSString *)countryNameFromCountryCode:(NSString *)countryCode; + (NSArray *)countryCodesForSearchTerm:(nullable NSString *)searchTerm; // Returns a list of country codes for a calling code in descending // order of population. -- (NSArray *)countryCodesFromCallingCode:(NSString *)callingCode; +- (NSArray *)countryCodesFromCallingCode:(NSString *)callingCode; // Returns the most likely country code for a calling code based on population. - (NSString *)probableCountryCodeForCallingCode:(NSString *)callingCode; diff --git a/SignalServiceKit/src/Contacts/PhoneNumberUtil.m b/SignalServiceKit/src/Contacts/PhoneNumberUtil.m index 359f0a9ab..cbf6bfa31 100644 --- a/SignalServiceKit/src/Contacts/PhoneNumberUtil.m +++ b/SignalServiceKit/src/Contacts/PhoneNumberUtil.m @@ -82,7 +82,8 @@ NS_ASSUME_NONNULL_BEGIN } // country code -> country name -+ (NSString *)countryNameFromCountryCode:(NSString *)countryCode { ++ (nullable NSString *)countryNameFromCountryCode:(NSString *)countryCode +{ OWSAssertDebug(countryCode); NSDictionary *countryCodeComponent = @{NSLocaleCountryCode : countryCode}; diff --git a/SignalServiceKit/src/TestUtils/TestAppContext.m b/SignalServiceKit/src/TestUtils/TestAppContext.m index 0fc67f5fe..4ff166493 100644 --- a/SignalServiceKit/src/TestUtils/TestAppContext.m +++ b/SignalServiceKit/src/TestUtils/TestAppContext.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "TestAppContext.h" @@ -148,6 +148,15 @@ NS_ASSUME_NONNULL_BEGIN return self.mockAppSharedDataDirectoryPath; } +- (BOOL)isDebugBuild +{ +#ifdef DEBUG + return YES; +#else + return NO; +#endif +} + @end #endif diff --git a/SignalServiceKit/src/Util/AppContext.h b/SignalServiceKit/src/Util/AppContext.h index a00aeeadf..5ebd5eb0d 100755 --- a/SignalServiceKit/src/Util/AppContext.h +++ b/SignalServiceKit/src/Util/AppContext.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // NS_ASSUME_NONNULL_BEGIN @@ -102,6 +102,8 @@ NSString *NSStringForUIApplicationState(UIApplicationState value); - (NSUserDefaults *)appUserDefaults; +- (BOOL)isDebugBuild; + @end id CurrentAppContext(void); diff --git a/SignalServiceKit/src/Util/String+SSK.swift b/SignalServiceKit/src/Util/String+SSK.swift index 3656b3ff9..b3bdca923 100644 --- a/SignalServiceKit/src/Util/String+SSK.swift +++ b/SignalServiceKit/src/Util/String+SSK.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 @@ -8,4 +8,8 @@ public extension String { func rtlSafeAppend(_ string: String) -> String { return (self as NSString).rtlSafeAppend(string) } + + public func substring(from index: Int) -> String { + return String(self[self.index(self.startIndex, offsetBy: index)...]) + } } diff --git a/SignalShareExtension/utils/ShareAppExtensionContext.m b/SignalShareExtension/utils/ShareAppExtensionContext.m index 0fd08c97a..7e9cc3e21 100644 --- a/SignalShareExtension/utils/ShareAppExtensionContext.m +++ b/SignalShareExtension/utils/ShareAppExtensionContext.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "ShareAppExtensionContext.h" @@ -234,6 +234,15 @@ NS_ASSUME_NONNULL_BEGIN return [[NSUserDefaults alloc] initWithSuiteName:SignalApplicationGroup]; } +- (BOOL)isDebugBuild +{ +#ifdef DEBUG + return YES; +#else + return NO; +#endif +} + @end NS_ASSUME_NONNULL_END