diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 8d157d008..40575582f 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -77,6 +77,7 @@ 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 */; }; + 3448E1662215B313004B052E /* OnboardingCaptchaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448E1652215B313004B052E /* OnboardingCaptchaViewController.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 */; }; @@ -741,6 +742,7 @@ 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 = ""; }; + 3448E1652215B313004B052E /* OnboardingCaptchaViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingCaptchaViewController.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 @@ 340FC879204DAC8C007AEB0F /* CodeVerificationViewController.h */, 340FC877204DAC8C007AEB0F /* CodeVerificationViewController.m */, 3448E1612213585C004B052E /* OnboardingBaseViewController.swift */, + 3448E1652215B313004B052E /* OnboardingCaptchaViewController.swift */, 3448E15D221333F5004B052E /* OnboardingController.swift */, 3448E15B22133274004B052E /* OnboardingPermissionsViewController.swift */, 3448E16322135FFA004B052E /* OnboardingPhoneNumberViewController.swift */, @@ -3623,6 +3626,7 @@ 340FC8B5204DAC8D007AEB0F /* AboutTableViewController.m in Sources */, 34BECE2B1F74C12700D7438D /* DebugUIStress.m in Sources */, 340FC8B9204DAC8D007AEB0F /* UpdateGroupViewController.m in Sources */, + 3448E1662215B313004B052E /* OnboardingCaptchaViewController.swift in Sources */, 4574A5D61DD6704700C6B692 /* CallService.swift in Sources */, 340FC8A8204DAC8D007AEB0F /* CodeVerificationViewController.m in Sources */, 4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */, diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 4c4c7f017..ee6bfbbfe 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -1479,7 +1479,7 @@ static NSTimeInterval launchStartedAt; } } else { if (OWSIsDebugBuild()) { - rootViewController = [[OnboardingControllerImpl new] initialViewController]; + rootViewController = [[OnboardingController new] initialViewController]; } else { rootViewController = [RegistrationViewController new]; } diff --git a/Signal/src/ViewControllers/Registration/CodeVerificationViewController.m b/Signal/src/ViewControllers/Registration/CodeVerificationViewController.m index 3a168ebd5..923955f0b 100644 --- a/Signal/src/ViewControllers/Registration/CodeVerificationViewController.m +++ b/Signal/src/ViewControllers/Registration/CodeVerificationViewController.m @@ -340,8 +340,8 @@ NS_ASSUME_NONNULL_BEGIN [_requestCodeAgainSpinner startAnimating]; __weak CodeVerificationViewController *weakSelf = self; - [self.tsAccountManager - rerequestSMSWithSuccess:^{ + [self.tsAccountManager rerequestSMSWithCaptchaToken:nil + success:^{ OWSLogInfo(@"Successfully requested SMS code"); [weakSelf enableServerActions:YES]; [weakSelf.requestCodeAgainSpinner stopAnimating]; @@ -363,8 +363,8 @@ NS_ASSUME_NONNULL_BEGIN [_requestCallSpinner startAnimating]; __weak CodeVerificationViewController *weakSelf = self; - [self.tsAccountManager - rerequestVoiceWithSuccess:^{ + [self.tsAccountManager rerequestVoiceWithCaptchaToken:nil + success:^{ OWSLogInfo(@"Successfully requested voice code"); [weakSelf enableServerActions:YES]; diff --git a/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift index 52cc3ff28..d242b81e2 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingBaseViewController.swift @@ -7,6 +7,7 @@ import PromiseKit @objc public class OnboardingBaseViewController: OWSViewController { + // Unlike a delegate, we can and should retain a strong reference to the OnboardingController. let onboardingController: OnboardingController @@ -24,7 +25,7 @@ public class OnboardingBaseViewController: OWSViewController { notImplemented() } - // MARK: - + // MARK: - Factory Methods func titleLabel(text: String) -> UILabel { let titleLabel = UILabel() @@ -69,7 +70,20 @@ public class OnboardingBaseViewController: OWSViewController { return button } - // MARK: Orientation + // MARK: - View Lifecycle + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // TODO: Is there a better way to do this? + if let navigationController = self.navigationController as? OWSNavigationController { + SignalApp.shared().signUpFlowNavigationController = navigationController + } else { + owsFailDebug("Missing or invalid navigationController") + } + } + + // MARK: - Orientation public override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .portrait diff --git a/Signal/src/ViewControllers/Registration/OnboardingCaptchaViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingCaptchaViewController.swift new file mode 100644 index 000000000..d9e152a8b --- /dev/null +++ b/Signal/src/ViewControllers/Registration/OnboardingCaptchaViewController.swift @@ -0,0 +1,199 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import UIKit +import WebKit + +@objc +public class OnboardingCaptchaViewController: OnboardingBaseViewController { + + private var webView: WKWebView? + + override public func loadView() { + super.loadView() + + 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_CAPTCHA_TITLE", comment: "Title of the 'onboarding Captcha' view.")) + + let titleRow = UIStackView(arrangedSubviews: [ + titleLabel + ]) + titleRow.axis = .vertical + titleRow.alignment = .fill + titleRow.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 0, right: 16) + titleRow.isLayoutMarginsRelativeArrangement = true + + // We want the CAPTCHA web content to "fill the screen (honoring margins)". + // The way to do this with WKWebView is to inject a javascript snippet that + // manipulates the viewport. + let jscript = "var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);" + let userScript = WKUserScript(source: jscript, injectionTime: .atDocumentEnd, forMainFrameOnly: true) + let wkUController = WKUserContentController() + wkUController.addUserScript(userScript) + let wkWebConfig = WKWebViewConfiguration() + wkWebConfig.userContentController = wkUController + let webView = WKWebView(frame: self.view.bounds, configuration: wkWebConfig) + self.webView = webView + webView.navigationDelegate = self + webView.allowsBackForwardNavigationGestures = false + webView.customUserAgent = "Signal iOS (+https://signal.org/download)" + webView.allowsLinkPreview = false + webView.scrollView.contentInset = .zero + webView.layoutMargins = .zero + + let stackView = UIStackView(arrangedSubviews: [ + titleRow, + webView + ]) + stackView.axis = .vertical + stackView.alignment = .fill + stackView.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + stackView.isLayoutMarginsRelativeArrangement = true + view.addSubview(stackView) + stackView.autoPinWidthToSuperviewMargins() + stackView.autoPin(toTopLayoutGuideOf: self, withInset: 0) + autoPinView(toBottomOfViewControllerOrKeyboard: stackView, avoidNotch: true) + + NotificationCenter.default.addObserver(self, + selector: #selector(didBecomeActive), + name: NSNotification.Name.OWSApplicationDidBecomeActive, + object: nil) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.navigationController?.isNavigationBarHidden = false + + loadContent() + } + + fileprivate let contentUrl = "https://signalcaptchas.org/registration/generate.html" + + private func loadContent() { + guard let webView = webView else { + owsFailDebug("Missing webView.") + return + } + guard let url = URL(string: contentUrl) else { + owsFailDebug("Invalid URL.") + return + } + webView.load(URLRequest(url: url)) + webView.scrollView.contentOffset = .zero + } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.navigationController?.isNavigationBarHidden = false + } + + // MARK: - Notifications + + @objc func didBecomeActive() { + AssertIsOnMainThread() + + loadContent() + } + + // MARK: - + + private func parseCaptchaAndTryToRegister(url: URL) { + Logger.info("") + + guard let captchaToken = parseCaptcha(url: url) else { + owsFailDebug("Could not parse captcha token: \(url)") + // TODO: Alert? + // + // Reload content so user can try again. + loadContent() + return + } + onboardingController.update(captchaToken: captchaToken) + + onboardingController.tryToRegister(fromViewController: self, smsVerification: false) + } + + private func parseCaptcha(url: URL) -> String? { + Logger.info("") + + // Example URL: + // signalcaptcha://03AF6jDqXgf1PocNNrWRJEENZ9l6RAMIsUoESi2dFKkxTgE2qjdZGVjEW6SZNFQqeRRTgGqOii6zHGG--uLyC1HnhSmRt8wHeKxHcg1hsK4ucTusANIeFXVB8wPPiV7U_0w2jUFVak5clMCvW9_JBfbfzj51_e9sou8DYfwc_R6THuTBTdpSV8Nh0yJalgget-nSukCxh6FPA6hRVbw7lP3r-me1QCykHOfh-V29UVaQ4Fs5upHvwB5rtiViqT_HN8WuGmdIdGcaWxaqy1lQTgFSs2Shdj593wZiXfhJnCWAw9rMn3jSgIZhkFxdXwKOmslQ2E_I8iWkm6 + guard let host = url.host, + host.count > 0 else { + owsFailDebug("Missing host.") + return nil + } + + return host + } +} + +// MARK: - + +extension OnboardingCaptchaViewController: WKNavigationDelegate { + public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + Logger.verbose("navigationAction: \(String(describing: navigationAction.request.url))") + + guard let url: URL = navigationAction.request.url else { + owsFailDebug("Missing URL.") + decisionHandler(.cancel) + return + } + if url.scheme == "signalcaptcha" { + decisionHandler(.cancel) + DispatchQueue.main.async { + self.parseCaptchaAndTryToRegister(url: url) + } + return + } + + // Loading the Captcha content involves a series of actions. + decisionHandler(.allow) + } + + public func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { + Logger.verbose("navigationResponse: \(String(describing: navigationResponse))") + + decisionHandler(.allow) + } + + public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + Logger.verbose("navigation: \(String(describing: navigation))") + } + + public func webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) { + Logger.verbose("navigation: \(String(describing: navigation))") + } + + public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + Logger.verbose("navigation: \(String(describing: navigation)), error: \(error)") + } + + public func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { + Logger.verbose("navigation: \(String(describing: navigation))") + } + + public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + Logger.verbose("navigation: \(String(describing: navigation))") + } + + public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + Logger.verbose("navigation: \(String(describing: navigation)), error: \(error)") + } + + public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { + Logger.verbose("") + } +} diff --git a/Signal/src/ViewControllers/Registration/OnboardingController.swift b/Signal/src/ViewControllers/Registration/OnboardingController.swift index fca6a964d..532cc975f 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingController.swift @@ -5,46 +5,285 @@ import UIKit @objc -public protocol OnboardingController: class { - func initialViewController() -> UIViewController +public class OnboardingCountryState: NSObject { + public let countryName: String + public let callingCode: String + public let countryCode: String - func onboardingSplashDidComplete(viewController: UIViewController) + @objc + public init(countryName: String, + callingCode: String, + countryCode: String) { + self.countryName = countryName + self.callingCode = callingCode + self.countryCode = countryCode + } + + public static var defaultValue: OnboardingCountryState { + AssertIsOnMainThread() + + var countryCode: String = PhoneNumber.defaultCountryCode() + if let lastRegisteredCountryCode = OnboardingController.lastRegisteredCountryCode(), + lastRegisteredCountryCode.count > 0 { + countryCode = lastRegisteredCountryCode + } - func onboardingPermissionsWasSkipped(viewController: UIViewController) - func onboardingPermissionsDidComplete(viewController: UIViewController) + let callingCodeNumber: NSNumber = PhoneNumberUtil.sharedThreadLocal().nbPhoneNumberUtil.getCountryCode(forRegion: countryCode) + let callingCode = "\(COUNTRY_CODE_PREFIX)\(callingCodeNumber)" - func onboardingPhoneNumberDidComplete(viewController: UIViewController) + var countryName = NSLocalizedString("UNKNOWN_COUNTRY_NAME", comment: "Label for unknown countries.") + if let countryNameDerived = PhoneNumberUtil.countryName(fromCountryCode: countryCode) { + countryName = countryNameDerived + } + + return OnboardingCountryState(countryName: countryName, callingCode: callingCode, countryCode: countryCode) + } } // MARK: - @objc -public class OnboardingControllerImpl: NSObject, OnboardingController { +public class OnboardingPhoneNumber: NSObject { + public let e164: String + public let userInput: String + + @objc + public init(e164: String, + userInput: String) { + self.e164 = e164 + self.userInput = userInput + } +} + +// MARK: - + +@objc +public class OnboardingController: NSObject { + + // MARK: - Dependencies + + private var tsAccountManager: TSAccountManager { + return TSAccountManager.sharedInstance() + } + + // MARK: - + + @objc + public override init() { + super.init() + } + + // MARK: - Factory Methods + + @objc public func initialViewController() -> UIViewController { + AssertIsOnMainThread() + let view = OnboardingSplashViewController(onboardingController: self) return view } + // MARK: - Transitions + public func onboardingSplashDidComplete(viewController: UIViewController) { + AssertIsOnMainThread() + + Logger.info("") + let view = OnboardingPermissionsViewController(onboardingController: self) viewController.navigationController?.pushViewController(view, animated: true) } public func onboardingPermissionsWasSkipped(viewController: UIViewController) { + AssertIsOnMainThread() + + Logger.info("") + pushPhoneNumberView(viewController: viewController) } public func onboardingPermissionsDidComplete(viewController: UIViewController) { + AssertIsOnMainThread() + + Logger.info("") + pushPhoneNumberView(viewController: viewController) } private func pushPhoneNumberView(viewController: UIViewController) { + AssertIsOnMainThread() + let view = OnboardingPhoneNumberViewController(onboardingController: self) viewController.navigationController?.pushViewController(view, animated: true) } - public func onboardingPhoneNumberDidComplete(viewController: UIViewController) { + public func onboardingRegistrationSucceeded(viewController: UIViewController) { + AssertIsOnMainThread() + + Logger.info("") + // CodeVerificationViewController *vc = [CodeVerificationViewController new]; // [weakSelf.navigationController pushViewController:vc animated:YES]; } + + public func onboardingDidRequireCaptcha(viewController: UIViewController) { + AssertIsOnMainThread() + + Logger.info("") + + guard let navigationController = viewController.navigationController else { + owsFailDebug("Missing navigationController.") + return + } + + // The service could demand CAPTCHA from the "phone number" view or later + // from the "code verification" view. The "Captcha" view should always appear + // immediately after the "phone number" view. + while navigationController.viewControllers.count > 1 && + !(navigationController.topViewController is OnboardingPhoneNumberViewController) { + navigationController.popViewController(animated: false) + } + + let view = OnboardingCaptchaViewController(onboardingController: self) + navigationController.pushViewController(view, animated: true) + } + + // MARK: - State + + public private(set) var countryState: OnboardingCountryState = .defaultValue + + public private(set) var phoneNumber: OnboardingPhoneNumber? + + public private(set) var captchaToken: String? + + @objc + public func update(countryState: OnboardingCountryState) { + AssertIsOnMainThread() + + self.countryState = countryState + } + + @objc + public func update(phoneNumber: OnboardingPhoneNumber) { + AssertIsOnMainThread() + + self.phoneNumber = phoneNumber + } + + @objc + public func update(captchaToken: String) { + AssertIsOnMainThread() + + self.captchaToken = captchaToken + } + + // MARK: - Debug + + private static let kKeychainService_LastRegistered = "kKeychainService_LastRegistered" + private static let kKeychainKey_LastRegisteredCountryCode = "kKeychainKey_LastRegisteredCountryCode" + private static let kKeychainKey_LastRegisteredPhoneNumber = "kKeychainKey_LastRegisteredPhoneNumber" + + private class func debugValue(forKey key: String) -> String? { + AssertIsOnMainThread() + + guard OWSIsDebugBuild() else { + return nil + } + + do { + let value = try CurrentAppContext().keychainStorage().string(forService: kKeychainService_LastRegistered, key: key) + return value + } catch { + owsFailDebug("Error: \(error)") + return nil + } + } + + private class func setDebugValue(_ value: String, forKey key: String) { + AssertIsOnMainThread() + + guard OWSIsDebugBuild() else { + return + } + + do { + try CurrentAppContext().keychainStorage().set(string: value, service: kKeychainService_LastRegistered, key: key) + } catch { + owsFailDebug("Error: \(error)") + } + } + + public class func lastRegisteredCountryCode() -> String? { + return debugValue(forKey: kKeychainKey_LastRegisteredCountryCode) + } + + private class func setLastRegisteredCountryCode(value: String) { + setDebugValue(value, forKey: kKeychainKey_LastRegisteredCountryCode) + } + + public class func lastRegisteredPhoneNumber() -> String? { + return debugValue(forKey: kKeychainKey_LastRegisteredPhoneNumber) + } + + private class func setLastRegisteredPhoneNumber(value: String) { + setDebugValue(value, forKey: kKeychainKey_LastRegisteredPhoneNumber) + } + + // MARK: - Registration + + public func tryToRegister(fromViewController: UIViewController, + smsVerification: Bool) { + guard let phoneNumber = phoneNumber else { + owsFailDebug("Missing phoneNumber.") + return + } + + // We eagerly update this state, regardless of whether or not the + // registration request succeeds. + OnboardingController.setLastRegisteredCountryCode(value: countryState.countryCode) + OnboardingController.setLastRegisteredPhoneNumber(value: phoneNumber.userInput) + + let captchaToken = self.captchaToken + ModalActivityIndicatorViewController.present(fromViewController: fromViewController, + canCancel: true) { (modal) in + + self.tsAccountManager.register(withPhoneNumber: phoneNumber.e164, + captchaToken: captchaToken, + success: { + DispatchQueue.main.async { + modal.dismiss(completion: { + self.registrationSucceeded(viewController: fromViewController) + }) + } + }, failure: { (error) in + Logger.error("Error: \(error)") + + DispatchQueue.main.async { + modal.dismiss(completion: { + self.registrationFailed(viewController: fromViewController, error: error as NSError) + }) + } + }, smsVerification: smsVerification) + } + } + + private func registrationSucceeded(viewController: UIViewController) { + onboardingRegistrationSucceeded(viewController: viewController) + } + + private func registrationFailed(viewController: UIViewController, error: NSError) { + if error.code == 402 { + 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: "")) + + } else { + OWSAlerts.showAlert(title: error.localizedDescription, + message: error.localizedRecoverySuggestion) + } + } } diff --git a/Signal/src/ViewControllers/Registration/OnboardingPhoneNumberViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingPhoneNumberViewController.swift index 64b398946..8846ae7f2 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingPhoneNumberViewController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingPhoneNumberViewController.swift @@ -24,13 +24,6 @@ public class OnboardingPhoneNumberViewController: OnboardingBaseViewController { 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 @@ -123,7 +116,6 @@ public class OnboardingPhoneNumberViewController: OnboardingBaseViewController { stackView.isLayoutMarginsRelativeArrangement = true view.addSubview(stackView) stackView.autoPinWidthToSuperviewMargins() - stackView.autoPinWidthToSuperviewMargins() stackView.autoPin(toTopLayoutGuideOf: self, withInset: 0) autoPinView(toBottomOfViewControllerOrKeyboard: stackView, avoidNotch: true) @@ -173,13 +165,13 @@ public class OnboardingPhoneNumberViewController: OnboardingBaseViewController { owsFailDebug("Could not resume re-registration; couldn't parse phoneNumberE164.") return } - guard let callingCode = parsedPhoneNumber.getCountryCode() else { + guard let callingCodeNumeric = parsedPhoneNumber.getCountryCode() else { owsFailDebug("Could not resume re-registration; missing callingCode.") return } - let callingCodeText = "\(COUNTRY_CODE_PREFIX)\(callingCode)" + let callingCode = "\(COUNTRY_CODE_PREFIX)\(callingCodeNumeric)" let countryCodes: [String] = - PhoneNumberUtil.sharedThreadLocal().countryCodes(fromCallingCode: callingCodeText) + PhoneNumberUtil.sharedThreadLocal().countryCodes(fromCallingCode: callingCode) guard let countryCode = countryCodes.first else { owsFailDebug("Could not resume re-registration; unknown countryCode.") return @@ -188,52 +180,11 @@ public class OnboardingPhoneNumberViewController: OnboardingBaseViewController { owsFailDebug("Could not resume re-registration; unknown countryName.") return } - if !phoneNumberE164.hasPrefix(callingCodeText) { + if !phoneNumberE164.hasPrefix(callingCode) { 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() + let phoneNumberWithoutCallingCode = phoneNumberE164.substring(from: callingCode.count) guard countryCode.count > 0 else { owsFailDebug("Invalid country code.") @@ -248,62 +199,55 @@ public class OnboardingPhoneNumberViewController: OnboardingBaseViewController { return } - self.countryName = countryName - self.callingCode = callingCode - self.countryCode = countryCode + let countryState = OnboardingCountryState(countryName: countryName, callingCode: callingCode, countryCode: countryCode) + onboardingController.update(countryState: countryState) - countryNameLabel.text = countryName - callingCodeLabel.text = callingCode + updateState() - self.phoneNumberTextField.placeholder = ViewControllerUtils.examplePhoneNumber(forCountryCode: countryCode, callingCode: callingCode) + phoneNumberTextField.text = phoneNumberWithoutCallingCode + // Don't let user edit their phone number while re-registering. + phoneNumberTextField.isEnabled = false } - // MARK: - Debug - - private let kKeychainService_LastRegistered = "kKeychainService_LastRegistered" - private let kKeychainKey_LastRegisteredCountryCode = "kKeychainKey_LastRegisteredCountryCode" - private let kKeychainKey_LastRegisteredPhoneNumber = "kKeychainKey_LastRegisteredPhoneNumber" + // MARK: - - private func debugValue(forKey key: String) -> String? { - guard OWSIsDebugBuild() else { - return nil + private var countryName: String { + get { + return onboardingController.countryState.countryName } + } + private var callingCode: String { + get { + AssertIsOnMainThread() - do { - let value = try CurrentAppContext().keychainStorage().string(forService: kKeychainService_LastRegistered, key: key) - return value - } catch { - owsFailDebug("Error: \(error)") - return nil + return onboardingController.countryState.callingCode } } + private var countryCode: String { + get { + AssertIsOnMainThread() - private func setDebugValue(_ value: String, forKey key: String) { - guard OWSIsDebugBuild() else { - return + return onboardingController.countryState.countryCode } + } - do { - try CurrentAppContext().keychainStorage().set(string: value, service: kKeychainService_LastRegistered, key: key) - } catch { - owsFailDebug("Error: \(error)") + private func populateDefaults() { + if let lastRegisteredPhoneNumber = OnboardingController.lastRegisteredPhoneNumber(), + lastRegisteredPhoneNumber.count > 0, + lastRegisteredPhoneNumber.hasPrefix(callingCode) { + phoneNumberTextField.text = lastRegisteredPhoneNumber.substring(from: callingCode.count) } - } - private func lastRegisteredCountryCode() -> String? { - return debugValue(forKey: kKeychainKey_LastRegisteredCountryCode) + updateState() } - private func setLastRegisteredCountryCode(value: String) { - setDebugValue(value, forKey: kKeychainKey_LastRegisteredCountryCode) - } + private func updateState() { + AssertIsOnMainThread() - private func lastRegisteredPhoneNumber() -> String? { - return debugValue(forKey: kKeychainKey_LastRegisteredPhoneNumber) - } + countryNameLabel.text = countryName + callingCodeLabel.text = callingCode - private func setLastRegisteredPhoneNumber(value: String) { - setDebugValue(value, forKey: kKeychainKey_LastRegisteredPhoneNumber) + self.phoneNumberTextField.placeholder = ViewControllerUtils.examplePhoneNumber(forCountryCode: countryCode, callingCode: callingCode) } // MARK: - Events @@ -368,10 +312,11 @@ public class OnboardingPhoneNumberViewController: OnboardingBaseViewController { comment: "Message of alert indicating that users needs to enter a valid phone number to register.")) return } - let parsedPhoneNumber = localNumber.toE164() + let e164PhoneNumber = localNumber.toE164() + + onboardingController.update(phoneNumber: OnboardingPhoneNumber(e164: e164PhoneNumber, userInput: phoneNumberText)) 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", @@ -379,59 +324,11 @@ public class OnboardingPhoneNumberViewController: OnboardingBaseViewController { proceedTitle: NSLocalizedString("REGISTRATION_IPAD_CONFIRM_BUTTON", comment: "button text to proceed with registration when on an iPad"), proceedAction: { (_) in - self.tryToRegister(parsedPhoneNumber: parsedPhoneNumber, - phoneNumberText: phoneNumberText, - countryCode: countryCode) + self.onboardingController.tryToRegister(fromViewController: self, smsVerification: false) }) } else { - tryToRegister(parsedPhoneNumber: parsedPhoneNumber, - phoneNumberText: phoneNumberText, - countryCode: countryCode) - } - } - - private func tryToRegister(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) + onboardingController.tryToRegister(fromViewController: self, smsVerification: false) } - - phoneNumberTextField.becomeFirstResponder() } } @@ -448,7 +345,6 @@ extension OnboardingPhoneNumberViewController: UITextFieldDelegate { public func textFieldShouldReturn(_ textField: UITextField) -> Bool { parseAndTryToRegister() - textField.resignFirstResponder() return false } } @@ -470,7 +366,11 @@ extension OnboardingPhoneNumberViewController: CountryCodeViewControllerDelegate return } - update(withCountryName: countryName, callingCode: callingCode, countryCode: countryCode) + 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/src/ViewControllers/Registration/RegistrationViewController.m b/Signal/src/ViewControllers/Registration/RegistrationViewController.m index cbaf4172e..823d7385f 100644 --- a/Signal/src/ViewControllers/Registration/RegistrationViewController.m +++ b/Signal/src/ViewControllers/Registration/RegistrationViewController.m @@ -448,6 +448,7 @@ NSString *const kKeychainKey_LastRegisteredPhoneNumber = @"kKeychainKey_LastRegi __weak RegistrationViewController *weakSelf = self; [self.tsAccountManager registerWithPhoneNumber:parsedPhoneNumber + captchaToken:nil success:^{ OWSProdInfo([OWSAnalyticsEvents registrationRegisteredPhoneNumber]); diff --git a/Signal/src/util/RegistrationUtils.m b/Signal/src/util/RegistrationUtils.m index 2789b6108..9a245cff5 100644 --- a/Signal/src/util/RegistrationUtils.m +++ b/Signal/src/util/RegistrationUtils.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "RegistrationUtils.h" @@ -59,8 +59,8 @@ NS_ASSUME_NONNULL_BEGIN presentFromViewController:fromViewController canCancel:NO backgroundBlock:^(ModalActivityIndicatorViewController *modalActivityIndicator) { - [self.tsAccountManager - registerWithPhoneNumber:self.tsAccountManager.reregisterationPhoneNumber + [self.tsAccountManager registerWithPhoneNumber:self.tsAccountManager.reregisterationPhoneNumber + captchaToken:nil success:^{ OWSLogInfo(@"re-registering: send verification code succeeded."); diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index dfcb3443b..6de276e74 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1092,11 +1092,11 @@ /* Label for button that resets crop & rotation state. */ "IMAGE_EDITOR_RESET_BUTTON" = "Reset"; -/* Label for button that rotates image 90 degrees. */ -"IMAGE_EDITOR_ROTATE_90_BUTTON" = "Rotate 90°"; +/* Label for button that rotates image 45 degrees. */ +"IMAGE_EDITOR_ROTATE_45_BUTTON" = "Rotate 45°"; /* Label for button that rotates image 90 degrees. */ -"IMAGE_EDITOR_ROTATE_45_BUTTON" = "Rotate 45°"; +"IMAGE_EDITOR_ROTATE_90_BUTTON" = "Rotate 90°"; /* Momentarily shown to the user when attempting to select more images than is allowed. Embeds {{max number of items}} that can be shared. */ "IMAGE_PICKER_CAN_SELECT_NO_MORE_TOAST_FORMAT" = "You can't share more than %@ items."; @@ -1508,6 +1508,9 @@ /* No comment provided by engineer. */ "OK" = "OK"; +/* Title of the 'onboarding Captcha' view. */ +"ONBOARDING_CAPTCHA_TITLE" = "We need to verify that you're human"; + /* Explanation in the 'onboarding permissions' view. */ "ONBOARDING_PERMISSIONS_EXPLANATION" = "ONBOARDING_PERMISSIONS_EXPLANATION"; diff --git a/SignalServiceKit/src/Account/TSAccountManager.h b/SignalServiceKit/src/Account/TSAccountManager.h index 92c873603..a2aff043e 100644 --- a/SignalServiceKit/src/Account/TSAccountManager.h +++ b/SignalServiceKit/src/Account/TSAccountManager.h @@ -90,13 +90,18 @@ typedef NS_ENUM(NSUInteger, OWSRegistrationState) { #pragma mark - Register with phone number - (void)registerWithPhoneNumber:(NSString *)phoneNumber + captchaToken:(nullable NSString *)captchaToken success:(void (^)(void))successBlock failure:(void (^)(NSError *error))failureBlock smsVerification:(BOOL)isSMS; -- (void)rerequestSMSWithSuccess:(void (^)(void))successBlock failure:(void (^)(NSError *error))failureBlock; +- (void)rerequestSMSWithCaptchaToken:(nullable NSString *)captchaToken + success:(void (^)(void))successBlock + failure:(void (^)(NSError *error))failureBlock; -- (void)rerequestVoiceWithSuccess:(void (^)(void))successBlock failure:(void (^)(NSError *error))failureBlock; +- (void)rerequestVoiceWithCaptchaToken:(nullable NSString *)captchaToken + success:(void (^)(void))successBlock + failure:(void (^)(NSError *error))failureBlock; - (void)verifyAccountWithCode:(NSString *)verificationCode pin:(nullable NSString *)pin diff --git a/SignalServiceKit/src/Account/TSAccountManager.m b/SignalServiceKit/src/Account/TSAccountManager.m index d95b63c53..5e2d18a82 100644 --- a/SignalServiceKit/src/Account/TSAccountManager.m +++ b/SignalServiceKit/src/Account/TSAccountManager.m @@ -322,6 +322,7 @@ NSString *const TSAccountManager_NeedsAccountAttributesUpdateKey = @"TSAccountMa } - (void)registerWithPhoneNumber:(NSString *)phoneNumber + captchaToken:(nullable NSString *)captchaToken success:(void (^)(void))successBlock failure:(void (^)(NSError *error))failureBlock smsVerification:(BOOL)isSMS @@ -339,6 +340,7 @@ NSString *const TSAccountManager_NeedsAccountAttributesUpdateKey = @"TSAccountMa TSRequest *request = [OWSRequestFactory requestVerificationCodeRequestWithPhoneNumber:phoneNumber + captchaToken:captchaToken transport:(isSMS ? TSVerificationTransportSMS : TSVerificationTransportVoice)]; [[TSNetworkManager sharedManager] makeRequest:request @@ -357,20 +359,33 @@ NSString *const TSAccountManager_NeedsAccountAttributesUpdateKey = @"TSAccountMa }]; } -- (void)rerequestSMSWithSuccess:(void (^)(void))successBlock failure:(void (^)(NSError *error))failureBlock +- (void)rerequestSMSWithCaptchaToken:(nullable NSString *)captchaToken + success:(void (^)(void))successBlock + failure:(void (^)(NSError *error))failureBlock { + // TODO: Can we remove phoneNumberAwaitingVerification? NSString *number = self.phoneNumberAwaitingVerification; OWSAssertDebug(number); - [self registerWithPhoneNumber:number success:successBlock failure:failureBlock smsVerification:YES]; + [self registerWithPhoneNumber:number + captchaToken:captchaToken + success:successBlock + failure:failureBlock + smsVerification:YES]; } -- (void)rerequestVoiceWithSuccess:(void (^)(void))successBlock failure:(void (^)(NSError *error))failureBlock +- (void)rerequestVoiceWithCaptchaToken:(nullable NSString *)captchaToken + success:(void (^)(void))successBlock + failure:(void (^)(NSError *error))failureBlock { NSString *number = self.phoneNumberAwaitingVerification; OWSAssertDebug(number); - [self registerWithPhoneNumber:number success:successBlock failure:failureBlock smsVerification:NO]; + [self registerWithPhoneNumber:number + captchaToken:captchaToken + success:successBlock + failure:failureBlock + smsVerification:NO]; } - (void)verifyAccountWithCode:(NSString *)verificationCode diff --git a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.h b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.h index b7ef07a7c..82da3e28f 100644 --- a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.h +++ b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.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 @@ -56,6 +56,7 @@ typedef NS_ENUM(NSUInteger, TSVerificationTransport) { TSVerificationTransportVo + (TSRequest *)unregisterAccountRequest; + (TSRequest *)requestVerificationCodeRequestWithPhoneNumber:(NSString *)phoneNumber + captchaToken:(nullable NSString *)captchaToken transport:(TSVerificationTransport)transport; + (TSRequest *)submitMessageRequestWithRecipient:(NSString *)recipientId diff --git a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m index bc2e4b6ae..c289cb3b4 100644 --- a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m +++ b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "OWSRequestFactory.h" @@ -235,13 +235,27 @@ NS_ASSUME_NONNULL_BEGIN } + (TSRequest *)requestVerificationCodeRequestWithPhoneNumber:(NSString *)phoneNumber + captchaToken:(nullable NSString *)captchaToken transport:(TSVerificationTransport)transport { OWSAssertDebug(phoneNumber.length > 0); - NSString *path = [NSString stringWithFormat:@"%@/%@/code/%@?client=ios", + + NSString *querystring = @"client=ios"; + if (captchaToken.length > 0) { + querystring = [NSString stringWithFormat:@"%@&captcha=%@", querystring, captchaToken]; + } + + NSURLQueryItem *screenNameItem, *includeRTsItem; + screenNameItem = [NSURLQueryItem queryItemWithName:@"screen_name" value:@"joemasilotti"]; + includeRTsItem = [NSURLQueryItem queryItemWithName:@"include_rts" value:@"true"]; + components.queryItems = @[ screenNameItem, includeRTsItem ]; + url = components.URL; + + NSString *path = [NSString stringWithFormat:@"%@/%@/code/%@?%@", textSecureAccountsAPI, [self stringForTransport:transport], - phoneNumber]; + phoneNumber, + querystring]; TSRequest *request = [TSRequest requestWithUrl:[NSURL URLWithString:path] method:@"GET" parameters:@{}]; request.shouldHaveAuthorizationHeaders = NO;